鞣製(Curry)


來看到Scala中以下的函式定義與使用:
def sum(a: Int, b: Int) = a + b
println(sum(1, 2)) // 顯示 3

如果改為以下的函式定義與使用,其結果也相同:
def sum(a: Int)(b: Int) = a + b
println(sum(1)(2)) // 顯示 3

第二個範例其實是使用了鞣製(Curry)的特性。所謂鞣製,是指將接受多個參數的函式(例如第一個範例的sum接受兩個參數),改為接受單一參數的函式(也就是第二個範例的sum(a: Int)),在函式執行過後傳回一個函式物件再套用剩下的參數(也就是第二個範例的(b: Int)),就像是將兩個函式鞣製在一起。

所以第二個範例,相當於以下的作用:
def sum(a: Int) = (b: Int) => a + b
println(sum(1)(2)) // 顯示 3

不過這只是個比喻,實際上你要取得呼叫鞣製函式的傳回函式物件,並不能像以下的範例:
def sum(a: Int)(b: Int) = a + b
val s = sum(1) // 這是錯的 missing arguments for method sum

若你真的要取得鞣製函式的傳回函式物件,可以使用 部份套用函式(Partially applied function) 的方式:
def sum(a: Int)(b: Int) = a + b
val s = sum(1)_
println(s(2)) // 顯示 3

在Scala中,結合鞣製函式的特性,可以讓你定義出一些函式,在呼叫使用時,感覺像是內建語法的一部份。要真正體驗這個特性之前,你還得先了解一件事,就是在Scala中,如果函式只接受一個引數,則引數列可以撰寫()或{},例如以下的範例都會顯示"XD":
println("XD")
println{"XD"}

了解這個特性之後,你可以知道,先前的sum範例也可以:
def sum(a: Int)(b: Int) = a + b
println(sum(1)(2)) // 顯示 3
println(sum(1){2})) // 顯示 3

因為sum(1)傳回函式物件後馬上套用第二個引數,而引數值只有一個,所以可以也使用{}來傳入引數。接著來看到 以名呼叫參數(By-name parameter) 中的這個範例:
def unless(cond: Boolean, expr: => Any) = {
if(!cond) {
expr
}
}

如果你使用鞣製的特性,把它改為以下的定義:
def unless(cond: Boolean)(expr: => Any) = {
if(!cond) {
expr
}
}

那麼,你可以如下使用unless函式:
val flag = false
unless(flag) {
println("XD")
println("Orz")
}

單看以上這個語法,你會感覺unless就像是內建的語法之一。另一個 以名呼叫參數(By-name parameter) 中的這個範例:
def until(cond: => Boolean, expr: => Unit) {
if(!cond) {
expr
until(cond, expr)
}
}

如果你使用鞣製的特性,把它改為以下的定義:
def until(cond: => Boolean)(expr: => Unit) {
if(!cond) {
expr
until(cond)(expr)
}
}

那麼你就可以如下使用until函式:
var count = 10
until(count == 0) {
println(count)
count -= 1
}

單看以上這個語法,你會感覺until就像是內建的語法之一。你可以再回憶一下 函 式重載、區域函式、重複參數 中,最後一個使用foldLeft別名方法/:的範例,這是因為/:也使用了鞣製的特性:
def sum(numbers: Int*) = (0 /: numbers) {_ + _}
println(sum(1, 2)) // 顯示 3