不只 OOP 有 Design Pattern,事實上 FP 也有不少 Pattern,而 Currying 算 FP 最基礎、使用最多的 Pattern。ECMAScript 雖然沒有直接支援,但因為有 First-class Function 、Lexical Scope,使得 Currying 在 ECMAScript 中使用成為可能。
Version
ECMAScript 2015
Definition
Currying
There is a way to reduce functions of more than one argument to functions of one argument, a way called currying將一個多 argument 的 function 改寫成多個只有一個 argument 的 function
Haskell B. Curry
Haskell B. Curry 是位數學家,為了紀念他,Haskell 語言是使用其 名,而 Curry 概念則是使用其 姓。
Simple Currying
let greeting = function(hi, target, name) {
return `${hi} ${target} ${name}`
}
greeting('Hello', 'World', 'Sam') // ?
我們以最簡單的 Hello World 為例,傳統 function 都會有多個 argument,在 greeting() 我們分別有 hi、target 與 name 3 個 argument。
根據 currying 定義,我們可將一個 3 個 argument 的 function,改寫成只有1個 argument 的 function。

let greeting = function(hi) {
return function(target) {
return function(name) {
return `${hi} ${target} ${name}`
}
}
}
greeting('Hello')('World')('Sam') // ?
由於 currying 要求每個 function 都只能有 1 個 argument,因此我們必須 return 兩次 function,直到最後一個 return 才會真正回傳值。
為什麼最內層的 function(name) 可以抓到 hi 與 target 呢 ? 拜 ECMAScript 的 lexical scope 之賜:
Lexical Scope
內層 function 可以直接存取到外層 funtion 之變數,而不必靠 argument 傳入
因此 function(name) 可直接使用 hi 與 target。
第 9 行
greeting('Hello')('World')('Sam'); // ?
因此 greeting('Hello') 回傳只有 1 個 argument 的 function,可再傳入 World。
而 greeting('Hello')('World') 亦回傳只有 1 個 argument 的 function,可再傳入 Sam。
所以 greeting('Hello')('World')('Sam') 其實相當於 greeting('Hello', 'World', 'Sam'),我們將原本 3 個 argument 的 function,變成各有 1 個 argument 的 3 個 function。

let greeting = hi => target => name => `${hi} ${target} ${name}`
greeting('Hello')('World')('Sam') // ?
拜 ES6 之賜,我們有了 arrow function,就不必再使用 巢狀 function 寫法,程式碼更簡潔,可讀性也變高,這也使得 currying 實用性大大提升。

Q:將傳統 function 改寫成 currying 不難,但為什麼要這樣寫呢 ?
的確,要改寫成 currying 並不難,尤其在 ES6 之後,arrow function 使得 currying 寫法非常精簡,也沒有必要再因為 巢狀 function 可讀性不高而排斥。
但回到一個更基本的問題,為什麼要使用 currying 這種 pattern 呢 ?
Reuse Function
拆成眾多的小 function,以利後續 code reuse。
let greeting = function(hi, target, name) {
return `${hi} ${target} ${name}`
}
若一次得傳入 3 個 argument,我們只有一個 greeting() function 可用。
let greeting = hi => target => name => `${hi} ${target} ${name}`
若改用 currying 寫法,我們總共有 3 個 function 可用:
greeting()greeting()()greeting()()()
在原本 greeting(),我們若要 reuse,一次就得提供 3 個 argument,否則就無法重複使用。
但 currying 過的 greeting(),變成了 3 個 function,我們可以依實際需求取用 greeting(),儘管只有 1 個 parameter,也一樣能夠使用 greeting()。
假設我們有個 function,只有 name 為 argument,回傳為 Hello World Sam 或 Hello World Kevin,原本 3 個 argument 的 greeting() 就無法被重複使用,但 currying 過的 greeting() 就能被重複使用。
Ramda 提供的 function 都使用 currying 形式,使得其 function 通常有兩種以上用法,一種是求值用,另一種則時產生 callback 用
let greeting = hi => target => name => `${hi} ${target} ${name}`
let helloWorld = greeting('Hello')('World')
helloWorld('Sam') // ?
第 3 行
let helloWorld = greeting('Hello')('World');
藉由 greeting('Hello')('World') 輕鬆建立新的 helloWorld() ,將來只接受 1 個 argument。
Currying 過的 greeting(),因為顆粒變小,因此能被 reuse 機會更高了。

回想小時候玩樂高積木,哪一種積木最好用 ?
就是顆粒最小的積木最好用,可以說是百搭。
Currying 就是把 function 都切成顆粒最小的單一 argument function,因此可藉由 argument 的組合,由一個 function 不斷地組合出新 function。
Higher Order Function
Higher Order Function
可以傳入 function 或傳回 function 的 function,通常會將重複部分抽成 higher order function,將不同部分以 arrow function 傳入,最後回傳新 function
要支援 higher order function 有個前提:語言本身必須支援 first-class function,這在 ECMAScript 很早就支援。
let data = [10, 20, 30]
let price1 = a => {
let sum = a => a.reduce((a, x) => a + x)
return sum(a) - 10
}
let price2 = a => {
let sum = a => a.reduce((a, x) => a + x)
return sum(a) * 0.9
}
price1(data) // ?
price2(data) // ?
第 3 行
let price1 = a => {
let sum = a => a.reduce((a, x) => a + x)
return sum(a) - 10
}
與
第 9 行
let price2 = a => {
let sum = a => a.reduce((a, x) => a + x)
return sum(a) * 0.9
}
非常類似,最少已經看到以下這部分重複:
let sum = a => a.reduce((a, x) => a + x)
return sum(a) - 10
所以想將這部分抽成 higher order function。

let data = [10, 20, 30]
let sum = a => a.reduce((a, x) => a + x)
let price = f => a => f(sum(a))
price(x => x - 10)(data) // ?
price(x => x * 0.9)(data) // ?
第 3 行
let sum = a => a.reduce((a, x) => a + x)
將 sum() 先抽成 function。
第 5 行
let price = f => a => f(sum(a))
將共用部分抽成 price() higher order function,argument 除了原本的 a 外,還多了 f,其中 f 正是 不同部分。
將 sum(a) 運算結果傳給 f() 執行。
第 7 行
price(x => x - 10)(data) // ?
price(x => x * 0.9)(data) // ?
將 不同部分 分別以 x => x -10 與 x => x * 0.9 帶入 price() higher order function,正式計算其值。

若我們不將 price() currying 過,則無法傳回 function,只能回傳值,如此就無法將 不同部分 以 arrow function 傳入。
Function Pipeline
let data = [10, 20, 30]
let discount = (rate, a) => a.map(x => x * rate)
let sum = a => a.reduce((a, x) => a + x)
let pipe = (...f) => init => f.reduce((g, f) => f(g), init)
pipe(
discount(0.8),
sum
)(data) // ?
第 3 行
let discount = (rate, a) => a.map(x => x * rate)
宣告 discount() ,使用傳統 2 個 argument 寫法。
第 5 行
let sum = a => a.reduce((a, x) => a + x)
宣告 sum(),使用 reduce() 計算 array 的總和。
第 7 行
let pipe = (...f) => init => f.reduce((g, f) => f(g), init)
自己寫一個 pipe() ,目的將所有 function 透過 reduce() 組合成一個新的 function。
實務上會使用 Ramda 的
pipe()
第 9 行
pipe(
discount(0.8),
sum
)(data) // ?
這裡會出問題,因為 discount() 尚未 currying,必須一次提供 2 個 argument,無法單獨只提供 0.8 一個 argument。
在純 FP 語言如 Haskell、F#、ReasonML 會自動 currying,所以不是問題,但 ECMAScript 必須手動 currying,或者使用 Ramda 的
curry()將原本 function 加以 currying

let data = [10, 20, 30]
let discount = rate => a => a.map(x => x * rate)
let sum = a => a.reduce((a, x) => a + x)
let pipe = (...f) => init => f.reduce((g, f) => f(g), init)
pipe(
discount(0.8),
sum
)(data) // ?
第 3 行
let discount = rate => a => a.map(x => x * rate)
將 discount() 改成 currying 寫法後,就可以使用 pipe() 將 sum() 與 discount() 組合成一個新 function。

為了使用 function composition,我們會將多個 argument 的 function,currying 成眾多單一 argument 的 function,然後再加以組合。
Conclusion
- ECMAScript 不像其他 FP 語言支援自動 currying,但所幸支援 first-class function 與 lexical scope,因此仍然可以手動將 function 加以 currying,或者透過 Ramda 的
curry() - Currying 會將 function 顆粒拆成更小,更有利於 reuse 與 compose,亦可透過 currying 回傳 higher order function,避免程式碼重複
Reference
歐陽繼超,前端函數式攻城指南
Martin Novak, JavaScript ES6 curry functions with practical examples
Adam Beme, Currying in JavaScript ES6
techsith, JavaScript Currying function (method) explained Tutorial