要怎樣定義一個人呢?看他一無所有時的耐心,及擁有一切時的態度。
--- 諺語
"你喜歡吃咖喱嗎?",一路淋著毛毛細雨,我們晃到一家餐廳。我注意到莊園裡的這個區域有種…特別的氣氛。路上的人明顯的比較少。而周遭的聲音也安靜了許多,稍微有點疏離感,但是是令人安心的那種。
我們坐定之後,小動物立刻把桌面上的東西清到旁邊,又開始畫了起來。
回傳函式的函式有許多種用法,但是就我就先來解釋其中最重要的術式吧。首先,我們要知道函式可以接受多個參數。
// JavaScript 語法
function addNumbers(x, y, z) {
return x + y + z
}
addNumbers(1, 2, 3) //=> 6
跟之前一樣,半空中出現了一個黃色的漏斗,這一個開口的部份特別寬,而且看起來相當軟。
那如果我們呼叫時,只給它一個參數呢?
// JavaScript 語法
addNumbers(1) //=> 運作正常, 回傳 NaN
NaN
代表"不是個數字"的意思。如果你很好奇為什麼的話,那是因為 JS 莊園的函式完全不檢查函式的參數數量 (arity)。如果呼叫時傳的參數不夠時,那麼它就會把缺少的參數,如本例中的 y
跟 z
設為 undefined
,代表尚未定義的值。
接下來當它試著把 1 + undefined + undefined
時,就會產生無法加總的結果了。
不管怎麼說,這真是個蠻糟糕的設計。
讓我們看一下 Ruby 公國是怎麼做的:
# Ruby 語法
def add_numbers(x, y, z)
x + y + z
end
出現一隻透明紅色的寬漏斗,而且旁邊還用吊飾綁了一個小寶石。不知道是材質還是其它原因,看起來就比較硬一點
# Ruby 語法
add_numbers(1)
# 爆炸了!
# 錯誤訊息: ArgumentError (wrong number of arguments (given 1, expected 3))
隨著爆炸的煙霧,漏斗也消失了
它們就會很明確的提示你傳的參數數量不夠,應該要三個參數的,你只給他一個,因此直接跳出執行期錯誤,這明顯比 JS 莊園的設計好多了。
而不管是 Ruby 公國或是 JS 莊園,當我們呼叫函式的時候,就會立刻把最終的答案算出來。因此我們一定要把參數都準備好了才能進行呼叫。我們將這一類函式稱為急躁的 (eager) 函式。在絕大多數的地區裡,函式大多是這一種的。
但是讓我們仔細想一下,除了直接明確的拒絕,或是囫圇吞棗的把沒傳的參數都改成 undefined
之外,有沒有什麼更好的做法呢?
讓我們把剛才的函式,改寫成這樣:
// JavaScript 語法
function lazyAddNumbers(x) { // 第一層
return function(y) { // 第二層
return function(z) { // 第三層
return x + y + z
}
}
}
「這是什麼鬼東西…」
雖然看起來字很多,但它真的很簡單。
它是個回傳回傳函式的函式的函式。
好啦我開玩笑的,你可以這樣看:這是一個回傳函式的函式,只是它是個三層的版本,而它的每一層只接收一個參數。
當我們這樣呼叫,會拿到什麼呢?
// JavaScript 語法
let step1 = lazyAddNumbers(1) //=> 拿到一個 function! 這是回傳第二層那行
// 做一些其它的事
let step2 = step1(2) //=> 又拿到一個 function! 這是回傳第三層那行
let result = step2(3) //=> 6
這種函式,一般被稱做懶惰的(lazy)的函式。但是我比較喜歡把它想成是一種有耐心的函式。它會在你給他的參數還不足時,善良的回傳一個函式給你,跟你說:「這些…還不夠進行計算呢。但是沒關係,我會在這裡等著,當您找到其它的參數時,再傳給這個新的函式噢。」 (為什麼變村上春樹語氣?)
「如果中間都一直回傳函式,那什麼時候才會回傳計算完成的值?」
好問題~ 當我們做出 n 層的懶惰函式之後,它就會需要 n 個參數才會開始計算。而當它拿到了所有它需要的參數時,魔法師們會稱呼這個狀態叫飽和 (saturate) 的。一旦飽和了,才會開始進行計算。
而當我們給這個函式的參數還不足夠的時候,我們會用數學的動詞 apply
,來描述這個函式是 partially applied、部分應用了的函式。而把partially apply 這個字名詞化,就是partial (function) application --- 函式被部份應用了。
當然你也可以使用莊園都更後的語法,看起來會…非常簡潔:
// JavaScript 語法
lazyAddNumbersVer2 = x => y => z => x + y + z
lazyAddNumbersVer2(1)(2)(3)
而我們剛剛手動做的,把一個接收三個參數的函式,改成三層而且每層各接收一個參數的函式這個改動的過程,有個名字,叫做… Currying。
「咖喱?我們正在吃的這個?」
嗯… 其實這是一個偉大的魔法師的姓變成的動詞,只是剛好拼起來一樣而己。一定要用你的語言的話,會叫「柯里化」。
而重要的是,這個過程,把 接受 n 個參數的函式,改成 n 層各接受一個參數的函式 這件事,是可以自動化的。不需要每次都手寫。讓我帶你去看吧。
我們離開餐館向外走,在同一區裡不太遠的地方停了下來。這座建築看起來像是個學院。門上鑲嵌著一個羊頭的標誌。而且形狀看起來像是個… 。
這個 Ramda 學院裡,有許多深刻理解了自由的函數這件事的人。他們知道 JS 莊園裡那些美好的本質,並且製造了許多專門給同樣理解函式自由的魔法師運用的術式。而自動柯里化,就是其中一個:
// JavaScript 語法,加上 Ramda
function addNumbers(x, y, z) { return x + y + z }
let lazyAddNumbers = R.curry(addNumbers); // 把函式本身傳進去
lazyAddNumbers(1, 2, 3) //=> 6
lazyAddNumbers(1)(2)(3) //=> 6
lazyAddNumbers(1, 2)(3) //=> 6
lazyAddNumbers(1)(2, 3) //=> 6
lazyAddNumbers(__, __, 3)(1)(2) //=> 也是 6,這就有點*太過魔法*了
在 Ruby 公國裡,你也可以手動做出一樣的東西:
# Ruby 語法
lazy_add_numbers = lambda {|x| lambda {|y| lambda {|z| x + y + z}}}
lazy_add_numbers.(1).(2).(3) #=> 6
不過在 Ruby 中,公國設計者很貼心的幫你做好了自動轉換的魔法:
# Ruby 語法
def add_all(x, y, z)
x + y + z
end
lazy_add_all = method(:add_all).curry() # 咖喱!!
#=> #<Proc:0x00007feb6f86f878 (lambda)>
lazy_add_all.(1).(2).(3) #=> 6
「這些讓我想到,有一種東西叫做 "閉包 (closure)"...」
啊…認真說起來,那比較像是在這些地區裡,回傳函式的函式這種手法的有趣特性吧。這種性質,等我們到神領 Haskell 時,可以有更清楚的理解。
那麼包括昨天陣列內建的 sort
、我們自己做的 cook
,還有今天這些回傳函式的函式,魔法師們對它們有個稱呼,叫做「高階函式」,意指「處理函式的函式」。而在所有的高階函式裡,有三種最為有名的…
「我知道,map
、reduce
跟 filter
對吧?小狸貓。」
是沒錯,但是你知道這三種東西,本質上是一樣的嗎?
還有…
「嗯?」
也不是狸貓。