一級函式(First-class function)


在Scala中,函式是一級(First-class)公民,也就是說,在Scala中,函式是物件。如果你要定義一個函式,基本上是使用def來定義,如同 簡單 的函式 中所說明過的,例如你要定義一個最大值的函式:
def max(m: Int, n: Int) = if(m > n) m else n

你可以用函式常量(Function literal)的方式來定義一個函式,執行時期將會為其產生函式值(Function value)。例如,上面的max函式,可以用以下的方式定義:
val max = (m: Int, n: Int) => if(m > n) m else n

你使用=>定義函式常量,在上例中,=>左邊的(m: Int, n: Int)定義了函式的參數與類型,=>右邊則是函式本體,max的型態呢?實際上是(Int, Int) => Int,也就是實際上完整的宣告應該是:
val max: (Int, Int) => Int = (m: Int, n: Int) => if(m > n) m else n

這說明了宣告函式物件的參考名稱時,如何指定型態型態,這表示你可以將一個函式物件進行傳遞,例如,在 選擇 排序 的實作中,你可以傳入一個函式物件來改變排序是要由小而大或由大而小:
def selection(number: Array[Int], order: (Int, Int) => Boolean) {
def mm(m: Int, j: Int): Int = {
if(j == number.length) m
else if(order(number(j), number(m))) mm(j, j + 1)
else mm(m, j + 1)
}
for(i <- 0 until number.length -1; m = mm(i, i + 1)
if i != m
) swap(number, i, m)
}

def swap(number: Array[Int], i: Int, j: Int) {
val t = number(i)
number(i) = number(j)
number(j) = t
}

如果你想要排序由小而大,則可以這麼使用函式:
val arr1 = Array(2, 5, 1, 7, 8)
selection(arr1, (a: Int, b: Int) => a < b)
println(arr1.mkString(",")) // 顯示 1,2,5,7,8

如果你想要排序由大而小,則可以這麼使用函式:
val arr2 = Array(2, 5, 1, 7, 8)
selection(arr2, (a: Int, b: Int) => a > b)
println(arr2.mkString(",")) // 顯示 8,7,5,2,1

你可以利用Scala的類型推斷來簡化函式字面量的撰寫方式,例如在上例中,可以從selection函式的參數宣告上,得知所傳入函式值的兩個參數型態,所以可以省略函式字面量撰寫時的參數型態。例如:
val arr2 = Array(2, 5, 1, 7, 8)
selection(arr2, (a, b) => a > b)
println(arr2.mkString(",")) // 顯示 8,7,5,2,1

如果函式字面在撰寫時,=>右邊的演算在使用參數時,有與參數相同的順序,則可以使用佔位字元語法(Placeholder syntax),省略參數列的宣告與=>的使用,例如:
val arr2 = Array(2, 5, 1, 7, 8)
selection(arr2, (_: Int) > (_: Int))
println(arr2.mkString(",")) // 顯示 8,7,5,2,1

上例中,第一個_代表傳入的第一個引數,第二個代表傳入的第二個引數,型態都是Int。如果可以利用Scala的類型推斷,則可以再簡化上例,例如:
val arr2 = Array(2, 5, 1, 7, 8)
selection(arr2, _ > _)
println(arr2.mkString(",")) // 顯示 8,7,5,2,1

使用佔位字元語法的方式,若要省略型態部份,必須在可以推斷類型的情況,例如:
val f = (_: Int) + (_: Int)    // 這樣 OK
println(f(1, 2)) // 顯示 3

但這樣就不行,因為Scala無法推斷出參數的類型為何:
val f = _ + _            // 錯誤 missing parameter type for expanded function

在Scala中,經常可以看到傳遞函式的寫法,例如群集物件的foreach方法,可以接受一個函式物件,當中定義如何處理群集中每個元素:
Array("a", "b", "c").foreach(x => print(x.toUpperCase)) // 顯示 ABC

要探討實際上類型推斷可以達到什麼程度會蠻複雜的,基本上建議的簡化撰寫原則是,在可以使用類型推斷的時候使用類型推斷,在無法使用類型推斷時,再標示出型態資訊。如果在簡單的函式定義中,參數的使用順序與參數列宣告的順序相同時,使用佔位字元寫法:
Array(4, 8, 1, 6, 3, 7, 9).filter(_ > 5).foreach(print(_))  // 顯示 8、6、7、9

如果你的函式字面量演算內容比較繁多,則可以使用{},例如:
val max = (m: Int, n: Int) => {
if(m > n)
m
else n
}

在Scala中,函式常量的寫法,其實會由編譯器自動產生出類別,並根據該類別建立函式物件,這個由編譯器動態產生的類別,有個apply方法,正如Scala語法的一致性,如果你想呼叫apply方法,其實可以直接使用(),這可以由以下的範例來證明:
val max = (m: Int, n: Int) => if(m > n) m else n
println(max(10, 20)) // 顯示 20
println(max.apply(10, 20)) // 顯示 20

所以實際上,使用def定義函式,與使用函式常量的寫法來產生函式物件是不同的,如果你要以def定義的函式來產生函式物件,則可以使用
部份套用函式(Partially applied function) 的語法。

在支援函式為一級物件的語言中,對於程式的撰寫可以有更多的彈性,例如,在
多 維矩陣轉一維矩陣 中,你可能原先設計了兩個函式:
def toOneByRow(array: Array[Array[Int]]) = {
val arr = new Array[Int](array.length * array(0).length)
for(row <- 0 until array.length; column <- 0 until array(0).length) {
arr(row * array(0).length + column) = array(row)(column)
}
arr
}

def toOneByColumn(array: Array[Array[Int]]) = {
val arr = new Array[Int](array.length * array(0).length)
for(row <- 0 until array.length; column <- 0 until array(0).length) {
arr(row + column * array.length) = array(row)(column)
}
arr
}

仔 細觀察,你會發現,除了粗體部份不同之外(也就是計算索引的部份),演算法的其它部份是相同的,演算實作時,這樣的重複結構並不鼓勵,如果將來你改變演算 法,則要修改一個函式時,複製至另一個函式,然後修改不同的部份(計算索引的部份),會造成維護上的麻煩。如果你可以傳遞函式物件,則可以改寫為以下的方 式:
def toOneByRow(array: Array[Array[Int]]) = {
toOne(array, _ * array(0).length + _)
}

def toOneByColumn(array: Array[Array[Int]]) = {
toOne(array, _ + _ * array.length)
}

def toOne(array: Array[Array[Int]], index: (Int, Int) => Int) = {
val arr = new Array[Int](array.length * array(0).length)
for(row <- 0 until array.length; column <- 0 until array(0).length) {
arr(index(row, column)) = array(row)(column)
}
arr
}