Scala 3 — 書籍

高階函數

語言

高階函數 (HOF) 通常定義為一個函數,它 (a) 將其他函數作為輸入參數,或 (b) 傳回一個函數作為結果。在 Scala 中,HOF 是可能的,因為函數是一等值。

作為一個重要的注意事項,雖然我們在本文中使用常見的產業術語「高階函數」,但在 Scala 中,此片語適用於方法函數。感謝 Scala 的 Eta 擴充技術,它們通常可以在相同的地方使用。

從消費者到創作者

在本書目前為止的範例中,您已看到如何成為將其他函式作為輸入參數的方法的使用者,例如使用 mapfilter 等 HOF。在接下來的幾個章節中,您將看到如何成為 HOF 的建立者,包括

  • 如何撰寫將函式作為輸入參數的方法
  • 如何從方法傳回函式

在此過程中,您將看到

  • 用於定義函式輸入參數的語法
  • 取得函式的參考後如何呼叫函式

作為此討論的有益副作用,一旦您熟悉此語法,您將使用它來定義函式參數、匿名函式和函式變數,而且也更容易閱讀高階函式的 Scaladoc。

了解 filter 的 Scaladoc

若要了解高階函式的運作方式,深入探討範例會有幫助。例如,您可以透過檢視其 Scaladoc 來了解 filter 接受的函式類型。以下是 List[A] 類別中的 filter 定義

def filter(p: A => Boolean): List[A]

這表示 filter 是方法,它會取得名為 p 的函式參數。根據慣例,p 代表謂詞,這只是一個傳回 Boolean 值的函式。因此 filter 會取得謂詞 p 作為輸入參數,並傳回 List[A],其中 A 是清單中所保存的類型;如果您在 List[Int] 上呼叫 filterA 就是類型 Int

在這個時候,如果您不知道 filter 方法的目的,您只會知道它的演算法會以某種方式使用謂詞 p 來建立並傳回 List[A]

特別檢視函式參數 pfilter 說明的這部分

p: A => Boolean

表示您傳入的任何函式都必須取得類型 A 作為輸入參數,並傳回 Boolean。因此如果您的清單是 List[Int],您可以將泛型類型 A 取代為 Int,並像這樣讀取該簽章

p: Int => Boolean

由於 isEven 具有此類型,它會將輸入 Int 轉換為結果 Boolean,因此可以使用 filter

撰寫採用函式參數的方法

在了解背景後,讓我們開始撰寫採用函式作為輸入參數的方法。

注意:為了讓以下討論更清楚,我們會將您撰寫的程式碼稱為方法,而您接受為輸入參數的程式碼稱為函式

第一個範例

若要建立採用函式參數的方法,您只需

  1. 在方法的參數清單中,定義您要接受的函式的簽章
  2. 在方法內使用該函式

為了示範,以下是一個採用名為 f 的輸入參數的方法,其中 f 是函式

def sayHello(f: () => Unit): Unit = f()

程式碼的這部分(類型簽章)指出 f 是函式,並定義 sayHello 方法將接受的函式類型

f: () => Unit

以下是其運作方式

  • f 是函式輸入參數的名稱。這就像將 String 參數命名為 s 或將 Int 參數命名為 i 一樣。
  • f 的類型簽章指定此方法將接受的函式的類型
  • f 簽章的 () 部分(在 => 符號的左側)指出 f 不採用任何輸入參數。
  • 簽章的 Unit 部分(在 => 符號的右側)指出 f 不應傳回有意義的結果。
  • 回顧 sayHello 方法的主體(在 = 符號的右側),那裡的 f() 陳述式會呼叫傳入的函式。

現在我們已定義 sayHello,讓我們建立一個函式以符合 f 的簽章,以便我們可以測試它。下列函式不採用任何輸入參數,也不會傳回任何內容,因此它符合 f 的類型簽章

def helloJoe(): Unit = println("Hello, Joe")

因為類型簽章相符,你可以傳遞 helloJoesayHello

sayHello(helloJoe)   // prints "Hello, Joe"

如果你從未這樣做過,恭喜:你剛剛定義了一個名為 sayHello 的方法,它將函式作為輸入參數,然後在方法主體中呼叫該函式。

sayHello 可以使用許多函式

重要的是要知道這種方法的優點不在於 sayHello 可以將 一個 函式作為輸入參數;優點在於它可以使用 任何f 的簽章相符的函式。例如,因為下一個函式不使用任何輸入參數且不傳回任何內容,所以它也可以與 sayHello 一起使用

def bonjourJulien(): Unit = println("Bonjour, Julien")

以下是 REPL 中的範例

scala> sayHello(bonjourJulien)
Bonjour, Julien

這是個好的開始。現在唯一要做的就是查看更多關於如何為函式參數定義不同類型簽章的範例。

定義函式輸入參數的一般語法

在此方法中

def sayHello(f: () => Unit): Unit

我們注意到 f 的類型簽章為

() => Unit

我們知道這表示「一個不使用任何輸入參數且不傳回任何有意義內容(由 Unit 給定)的函式」

為了示範更多類型簽章範例,以下是一個使用 String 參數並傳回 Int 的函式

f: String => Int

哪些函式會使用字串並傳回整數?「字串長度」和「檢查總和」函式就是兩個範例。

類似地,此函式使用兩個 Int 參數並傳回 Int

f: (Int, Int) => Int

你能想像哪些函式符合該簽章嗎?

答案是任何使用兩個 Int 輸入參數並傳回 Int 的函式都符合該簽章,因此所有這些「函式」(實際上是方法)都符合

def add(a: Int, b: Int): Int = a + b
def subtract(a: Int, b: Int): Int = a - b
def multiply(a: Int, b: Int): Int = a * b

從這些範例中可以推論,定義函式參數類型簽章的一般語法為

variableName: (parameterTypes ...) => returnType

由於函式式程式設計就像建立和組合一系列代數方程式,因此在設計函式和應用程式時,通常會 大量 思考類型。你可以說你「以類型思考」。

將函數參數與其他參數一起傳遞

若要讓 HOF 真正發揮作用,它們也需要一些資料來處理。對於像 List 這樣的類別,其 map 方法已經有資料可以處理:List 中的資料。但對於沒有自己資料的獨立 HOF,它也應該接受資料作為其他輸入參數。

例如,這裡有一個名為 executeNTimes 的方法,它有兩個輸入參數:一個函數和一個 Int

def executeNTimes(f: () => Unit, n: Int): Unit =
  for (i <- 1 to n) f()
def executeNTimes(f: () => Unit, n: Int): Unit =
  for i <- 1 to n do f()

正如程式碼所示,executeNTimes 執行 f 函數 n 次。由於像這樣的簡單 for 迴圈沒有傳回值,executeNTimes 傳回 Unit

若要測試 executeNTimes,請定義一個與 f 簽章相符的方法

// a method of type `() => Unit`
def helloWorld(): Unit = println("Hello, world")

然後將該方法傳遞到 executeNTimes 中,並附上一個 Int

scala> executeNTimes(helloWorld, 3)
Hello, world
Hello, world
Hello, world

太棒了。executeNTimes 方法執行 helloWorld 函數三次。

所需參數數量

您的方法可以繼續變得複雜,視需要而定。例如,此方法採用類型為 (Int, Int) => Int 的函數,以及兩個輸入參數

def executeAndPrint(f: (Int, Int) => Int, i: Int, j: Int): Unit =
  println(f(i, j))

由於這些 summultiply 方法與該類型簽章相符,因此它們可以與兩個 Int 值一起傳遞到 executeAndPrint

def sum(x: Int, y: Int) = x + y
def multiply(x: Int, y: Int) = x * y

executeAndPrint(sum, 3, 11)       // prints 14
executeAndPrint(multiply, 3, 9)   // prints 27

函數類型簽章一致性

學習 Scala 函數類型簽章的一大好處是,您用於定義函數輸入參數的語法與用於撰寫函數文字的語法相同。

例如,如果您要撰寫一個計算兩個整數總和的函數,則可以像這樣撰寫

val f: (Int, Int) => Int = (a, b) => a + b

該程式碼包含類型簽章

val f: (Int, Int) => Int = (a, b) => a + b
       -----------------

輸入參數

val f: (Int, Int) => Int = (a, b) => a + b
                           ------

以及函數主體

val f: (Int, Int) => Int = (a, b) => a + b
                                     -----

Scala 的一致性在此處顯示,其中此函數類型

val f: (Int, Int) => Int = (a, b) => a + b
       -----------------

與您用於定義函數輸入參數的類型簽章相同

def executeAndPrint(f: (Int, Int) => Int, ...
                       -----------------

一旦你熟悉此語法,你將使用它來定義函數參數、匿名函數和函數變數,並且更容易閱讀高階函數的 Scaladoc。

此頁面的貢獻者