Scala 3 — 書籍

純函數

語言

Scala 提供的另一個有助於撰寫函數式程式碼的功能,是撰寫純函數的能力。純函數可以這樣定義

  • 如果函數 f 是純函數,則在給定相同的輸入 x 時,它總是會傳回相同的輸出 f(x)
  • 函數的輸出取決於其輸入變數及其實作
  • 它只會計算輸出,而不會修改其周遭的世界

這表示

  • 它不會修改其輸入參數
  • 它不會變異任何隱藏狀態
  • 它沒有任何「後門」:它不會從外部世界(包括控制台、網路服務、資料庫、檔案等)讀取資料,或寫入資料到外部世界

根據這個定義,任何時候你使用相同的輸入值呼叫純函數,你都將會得到相同的結果。例如,你可以使用輸入值 2 無限次呼叫 double 函數,而你將會永遠得到結果 4

純函數範例

根據這個定義,你可以想像,scala.math._ 套件中的這些方法是純函數

  • abs
  • ceil
  • max

這些 String 方法也是純函數

  • isEmpty
  • length
  • substring

Scala 集合類別中的大多數方法也作為純函數運作,包括 dropfiltermap,以及更多。

在 Scala 中,函數方法 幾乎可以完全互換,因此即使我們使用常見的產業術語「純函數」,這個術語可以用來描述函數和方法。如果你有興趣了解方法如何像函數一樣使用,請參閱 Eta 展開 討論。

非純函數範例

相反地,下列函數是非純函數,因為它們違反了定義。

  • println – 與主控台、檔案、資料庫、網路服務、感測器等互動的方法都是不純的。
  • currentTimeMillis – 與日期和時間相關的方法都是不純的,因為其輸出取決於其輸入參數以外的其他因素
  • sys.error – 拋出例外的方法是不純的,因為它們不只回傳結果

不純函式通常會執行下列一項或多項操作

  • 從隱藏狀態讀取,亦即存取未明確傳遞至函式中作為輸入參數的變數和資料
  • 寫入隱藏狀態
  • 變更其所給定的參數,或變更隱藏變數,例如其所包含類別中的欄位
  • 執行某種形式的 I/O 與外界互動

一般來說,您應注意回傳類型為 Unit 的函式。由於這些函式不回傳任何內容,因此邏輯上您呼叫它們的唯一原因是為了達成某些副作用。因此,這些函式的使用通常是不純的。

但需要不純函式…

當然,如果應用程式無法讀取或寫入外界,就不會很有用,因此人們提出此建議

使用純函式撰寫應用程式的核心,然後在該核心周圍撰寫不純的「包裝器」以與外界互動。正如某人曾經說過的,這就像在純蛋糕上放一層不純的糖霜。

請務必注意,有方法可以讓不純的與外界互動感覺更純。例如,您會聽說使用 IO Monad 來處理輸入和輸出。這些主題超出了本文檔的範圍,因此為了簡化起見,可以認為 FP 應用程式有一個純函式核心,並用其他函式包裝起來與外界互動。

撰寫純函數

注意:本節中,常見的產業術語「純函數」通常用於指稱 Scala 方法。

若要撰寫 Scala 中的純函數,請使用 Scala 的方法語法撰寫(不過您也可以使用 Scala 的函數語法)。例如,以下是將輸入值加倍的純函數

def double(i: Int): Int = i * 2

如果您熟悉遞迴,以下是計算整數清單總和的純函數

def sum(xs: List[Int]): Int = xs match {
  case Nil => 0
  case head :: tail => head + sum(tail)
}
def sum(xs: List[Int]): Int = xs match
  case Nil => 0
  case head :: tail => head + sum(tail)

如果您了解該程式碼,您會發現它符合純函數定義。

重點

本節的第一個重點是純函數的定義

純函數是僅依賴其宣告的輸入和實作來產生輸出的函數。它只計算其輸出,不依賴或修改外部世界。

第二個重點是,每個真實世界的應用程式都會與外部世界互動。因此,思考函數式程式的一個簡化方式是,它們包含一個純函數核心,並以與外部世界互動的其他函數進行包裝。

本頁的貢獻者