iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 9
2

要怎樣定義一個人呢?看他一無所有時的耐心,及擁有一切時的態度。
--- 諺語


"你喜歡吃咖喱嗎?",一路淋著毛毛細雨,我們晃到一家餐廳。我注意到莊園裡的這個區域有種…特別的氣氛。路上的人明顯的比較少。而周遭的聲音也安靜了許多,稍微有點疏離感,但是是令人安心的那種。

我們坐定之後,小動物立刻把桌面上的東西清到旁邊,又開始畫了起來。


勤勞的函式

回傳函式的函式有許多種用法,但是就我就先來解釋其中最重要的術式吧。首先,我們要知道函式可以接受多個參數

// JavaScript 語法
function addNumbers(x, y, z) {
  return x + y + z
}
addNumbers(1, 2, 3) //=> 6

跟之前一樣,半空中出現了一個黃色的漏斗,這一個開口的部份特別寬,而且看起來相當軟。

那如果我們呼叫時,只給它一個參數呢?

// JavaScript 語法
addNumbers(1) //=> 運作正常, 回傳 NaN

NaN 代表"不是個數字"的意思。如果你很好奇為什麼的話,那是因為 JS 莊園的函式完全不檢查函式的參數數量 (arity)。如果呼叫時傳的參數不夠時,那麼它就會把缺少的參數,如本例中的 yz 設為 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,自動的。

而我們剛剛手動做的,把一個接收三個參數的函式,改成三層而且每層各接收一個參數的函式這個改動的過程,有個名字,叫做… Currying

「咖喱?我們正在吃的這個?」

嗯… 其實這是一個偉大的魔法師的姓變成的動詞,只是剛好拼起來一樣而己。一定要用你的語言的話,會叫「柯里化」。

而重要的是,這個過程,把 接受 n 個參數的函式,改成 n 層各接受一個參數的函式 這件事,是可以自動化的。不需要每次都手寫。讓我帶你去看吧。


我們離開餐館向外走,在同一區裡不太遠的地方停了下來。這座建築看起來像是個學院。門上鑲嵌著一個羊頭的標誌。而且形狀看起來像是個… https://chart.googleapis.com/chart?cht=tx&chl=%5Clambda


這個 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 公國的 Currying

在 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,還有今天這些回傳函式的函式,魔法師們對它們有個稱呼,叫做「高階函式」,意指「處理函式的函式」。而在所有的高階函式裡,有三種最為有名的…

「我知道,mapreducefilter 對吧?小狸貓。」

是沒錯,但是你知道這三種東西,本質上是一樣的嗎?

還有…

「嗯?」

也不是狸貓


上一篇
mostly:functional 第七章:不存在的名字、自由的樣貌
下一篇
mostly:functional 第九章:高階函式與它們的產地
系列文
mostly:functional 從零開始的異世界程式觀 --- 函數式程式設計的試煉35

尚未有邦友留言

立即登入留言