在〈認識 Lambda/Closure〉系列先前的文章中,我們使用了 JavaScript 與 Python 來示範 Lambda/Closure 為何,以及如何善用它們。就學習 Lambda/Closure 而言,這是個不錯的開始,因為 JavaScript 與 Python 都是動態語言,在這些語言中不用在意變數的型態。
然而在步入靜態語言的世界時,我們都知道編譯器需要型態訊息,以便在編譯時期檢查出各種可能的型態不符之錯誤。這點很有助益,因為可以在撰寫程式的一開始就捕捉到一些錯誤,降低因錯誤而帶來的成本負擔。不過在討論到程式碼的簡潔度時,靜態語言中冗長的型態宣告常是令人詬病的對象。
先來看看 Scala 中如何定義函式好了:
def max(m: Int, n: Int): Int = if(m > n) m else n
Scala 是靜態語言,所以必須宣告函式的參數型態為何。在這邊型態宣告似乎沒什麼大問題。嗯…來看看如何宣告匿名函式並指定給變數好了。
val max: (Int, Int) => Int = (m: Int, n: Int) => if(m > n) m else n
喔…看來語法一大垞!你必須聲明 max
的型態為 (Int, Int) => Int
,這專用的函式型態表示,此函式會接受兩個 Int
引數,而傳回值是 Int
。在定義匿名函式本身時,也必須宣告參數的型態,像是 (m: Int, n: Int) => if(m > n) m else n
。如果要定義一個可接受回呼函式的參數,也必須宣告該參數的函式型態。例如:
def bubbleSort(arr: Array[Int], order: (Int, Int) => Boolean): Unit {
...
val o: Boolean = order(a, b)
...
}
上例中,order
參數接受一個函式,該函式會有兩個 Int
參數,並傳回 Boolean
值。以下的程式碼示範了如何呼叫 bubbleSort
函式。
val arr: Array[Int] = Array(2, 5, 1, 7, 8)
bubbleSort(arr, (a: Int, b: Int) => a > b)
如果在 Scala 中真的得用這麼冗長的語法,你還會想用 Lambda/Closure 嗎?所幸地是,Scala 的編譯器很聰明,能夠進行型態推斷(Type inference)。它能夠從原始碼前後文推斷出型態資訊,所以實際上在宣告變數或撰寫匿名函式時,有很大的機會是不用宣告型態的。例如,實際上先前的程式碼可以重新撰寫為以下的形式:
val arr = Array(2, 5, 1, 7, 8)
bubbleSort(arr, (a, b) => a > b)
在上例中,Scala 編譯器從 Array(2, 5, 1, 7, 8)
的程式碼中推斷出 arr
的型態會是 Array[Int]
,所以你就不用作宣告了。而且,Scala 編譯器可以從 arr
推斷,匿名函式將接受的引數型態會是 Int
,所以實際上你只要提供參數名稱及函式本體就可以了。事實上在 Scala 中,你還可以用更短的語法來呼叫該函式。例如:
val arr = Array(2, 5, 1, 7, 8)
bubbleSort(arr, (_: Int) > (_: Int))
或者,甚至是以下最短的語法:
val arr = Array(2, 5, 1, 7, 8)
bubbleSort(arr, _ > _)
在這邊不打算解釋 Scala 是如何完成這類的魔法,如果有興趣瞭解的話,可以進一步看看 Scala 學習筆記。這邊的重點在於,對於靜態語言來說,類型推斷是很重要的功能。在必須提供型態資訊的場合中,類型推斷可以讓程式碼簡潔易讀,像是在宣告變數或者是撰寫匿名函式的時候。Lambda/Closure 是個表達工具,如果沒有型態推斷,過於冗長的語法只會讓開發者望之卻步,不可能讓他們有意願採用。在下一步文章中,就會來談談 Java 中的 Lambda/Closure。不過,實際上會先來看個 舊的提案,這有助於我們瞭解 Lambda/Closure 何以會演變為現今 JDK8 中的形態。