簡介
柯里化從何而來
柯里化, 即 Currying 的音譯。 Currying 是編譯原理層面實現多參函式的一個技術。
在說JavaScript 中的柯里化前,可以聊一下原始的 Currying 是什麼,又從何而來。
在編碼過程中,身為碼農的我們本質上所進行的工作就是——將複雜問題分解為多個可程式設計的小問題。
Currying 為實現多參函式提供了一個遞迴降解的實現思路——把接受多個引數的函式變換成接受一個單一引數(最初函式的第一個引數)的函式,並且返回接受餘下的引數而且返回結果的新函式,在某些程式語言中(如 Haskell),是通過 Currying 技術支援多參函式這一語言特性的。
所以 Currying 原本是一門編譯原理層面的技術,用途是實現多參函式。
柯里化去向哪裡
在 Haskell 中,函式作為一等公民,Currying 從編譯原理層面的技術應運而成了一個語言特性。 在語言特性層面,Currying 是什麼?
在《Mostly adequate guide》一書中,這樣總結了 Currying ——只傳遞給函式一部分引數來呼叫它,讓它返回一個函式去處理剩下的引數。
所以 Currying 是應函數語言程式設計而生,在有了 Currying 後,大家再去探索去發掘了它的用途及意義。 然後因為這些用途和意義,大家才積極地將它擴充套件到其他程式語言中。
在 JavaScript 中實現 Currying
為了實現只傳遞給函式一部分引數來呼叫它,讓它返回一個函式去處理剩下的引數這句話所描述的特性。
我們先寫一個實現加法的函式 add
:
function add (x, y) {
return (x + y)
}
複製程式碼
現在我們直接實現一個被 Currying 的 add
函式,該函式名為 curriedAdd
,則根據上面的定義,curriedAdd
需要滿足以下條件:
curriedAdd(1)(3) === 4
// true
var increment = curriedAdd(1)
increment(2) === 3
// true
var addTen = curriedAdd(10)
addTen(2) === 12
// true
複製程式碼
滿足以上條件的 curriedAdd
的函式可以用以下程式碼段實現:
function curriedAdd (x) {
return function(y) {
return x + y
}
}
複製程式碼
當然以上實現是有一些問題的:它並不通用,並且我們並不想通過重新編碼函式本身的方式來實現 Currying 化。
但是這個 curriedAdd
的實現表明了實現 Currying 的一個基礎 —— Currying 延遲求值的特性需要用到 JavaScript 中的作用域——說得更通俗一些,我們需要使用作用域來儲存上一次傳進來的引數。
對 curriedAdd
進行抽象,可能會得到如下函式 currying
:
function currying (fn, ...args1) {
return function (...args2) {
return fn(...args1, ...args2)
}
}
var increment = currying(add, 1)
increment(2) === 3
// true
var addTen = currying(add, 10)
addTen(2) === 12
// true
複製程式碼
在此實現中,currying
函式的返回值其實是一個接收剩餘引數並且立即返回計算值的函式。即它的返回值並沒有自動被 Currying化 。所以我們可以通過遞迴來將 currying 的返回的函式也自動 Currying 化。
function trueCurrying(fn, ...args) {
if (args.length >= fn.length) {
return fn(...args)
}
return function (...args2) {
return trueCurrying(fn, ...args, ...args2)
}
}
複製程式碼
以上函式很簡短,但是已經實現 Currying 的核心思想了。JavaScript 中的常用庫 Lodash 中的 curry 方法,其核心思想和以上並沒有太大差異——比較多次接受的引數總數與函式定義時的入引數量,當接受引數的數量大於或等於被 Currying 函式的傳入引數數量時,就返回計算結果,否則返回一個繼續接受引數的函式。
Lodash 中實現 Currying 的程式碼段較長,因為它考慮了更多的事情,比如繫結 this 變數等。在此處就不直接貼出 Lodash 中的程式碼段,感興趣的同學可以去看看看 Lodash 原始碼,比較一下這兩種實現會導致什麼樣的差異。
然而 Currying 的定義和實現都不是最重要的,本文想要闡述的重點是:它能夠解決編碼和開發當中怎樣的問題,以及在面對不同的問題時,選擇一個合適的 Currying,來最恰當的解決問題。
Currying 使用場景
引數複用
固定不變的引數,實現引數複用是 Currying 的主要用途之一。
上文中的increment
, addTen
是一個引數複用的例項。對add
方法固定第一個引數為 10 後,改方法就變成了一個將接受的變數值加 10 的方法。
延遲執行
延遲執行也是 Currying 的一個重要使用場景,同樣 bind 和箭頭函式也能實現同樣的功能。
在前端開發中,一個常見的場景就是為標籤繫結 onClick 事件,同時考慮為繫結的方法傳遞引數。
以下列出了幾種常見的方法,來比較優劣:
-
通過 data 屬性
<div data-name="name" onClick={handleOnClick} /> 複製程式碼
通過 data 屬性本質只能傳遞字串的資料,如果需要傳遞複雜物件,只能通過
JSON.stringify(data)
來傳遞滿足 JSON 物件格式的資料,但對更加複雜的物件無法支援。(雖然大多數時候也無需傳遞複雜物件) -
通過bind方法
<div onClick={handleOnClick.bind(null, data)} /> 複製程式碼
bind 方法和以上實現的
currying
方法,在功能上有極大的相似,在實現上也幾乎差不多。可能唯一的不同就是 bind 方法需要強制繫結 context,也就是 bind 的第一個引數會作為原函式執行時的 this 指向。而currying
不需要此引數。所以使用currying
或者 bind 只是一個取捨問題。 -
箭頭函式
<div onClick={() => handleOnClick(data))} /> 複製程式碼
箭頭函式能夠實現延遲執行,同時也不像 bind 方法必需指定 context。可能唯一需要顧慮的就是在 react 中,會有人反對在 jsx 標籤內寫箭頭函式,這樣子容易導致直接在 jsx 標籤內寫業務邏輯。
-
通過currying
<div onClick={currying(handleOnClick, data)} /> 複製程式碼
效能對比
通過 jsPerf
測試四種方式的效能,結果為:箭頭函式
>bind
>currying
>trueCurrying
。
currying 函式相比 bind 函式,其原理相似,但是效能相差巨大,其原因是 bind 由瀏覽器實現,執行效率有加成。
從這個結果看 Currying 效能無疑是最差的,但是另一方面就算最差的 trueCurrying
的實現,也能在本人的個人電腦上達到 50w Ops/s 的情況下,說明這些效能是無需在意的。
而 trueCurrying
方法中實現的自動 Currying 化,是另外三個方法所不具備的。
到底需不需要 Currying
為什麼需要 Currying
-
為了多參函式複用性
Currying 讓人眼前一亮的地方在於,讓人覺得函式還能這樣子複用。
通過一行程式碼,將
add
函式轉換為 increment,addTen 等。對於 Currying 的複雜實現中,以 Lodash 為列,提供了
placeholder
的神奇操作。對多參函式的複用玩出花樣。import _ from 'loadsh' function abc (a, b, c) { return [a, b, c]; } var curried = _.curry(abc) // Curried with placeholders. curried(1)(_, 3)(2) // => [1, 2, 3] 複製程式碼
-
為函數語言程式設計而生
Currying 是為函式式而生的東西。應運著有一整套函數語言程式設計的東西,
純函式
、compose
、container
等等事物。(可閱讀《mostly-adequate-guide》 )假如要寫 Pointfree Javascript 風格的程式碼,那麼Currying是不可或缺的。
要使用 compose,要使用 container 等事物,我們也需要 Currying。
為什麼不需要 Currying
-
Currying 的一些特性有其他解決方案
如果我們只是想提前繫結引數,那麼我們有很多好幾個現成的選擇,bind,箭頭函式等,而且效能比Curring更好。
-
Currying 陷於函數語言程式設計
在本文中,提供了一個
trueCurrying
的實現,這個實現也是最符合 Currying 定義的,也提供 了bind,箭頭函式等不具備的“新奇”特性——可持續的 Currying(這個詞是本人臨時造的)。但是這個“新奇”特性的應用並非想象得那麼廣泛。
其原因在於,Currying 是函數語言程式設計的產物,它生於函數語言程式設計,也服務於函數語言程式設計。
而 JavaScript 並非真正的函數語言程式設計語言,相比 Haskell 等函數語言程式設計語言,JavaScript 使用 Currying 等函式式特性有額外的效能開銷,也缺乏型別推導。
從而把 JavaScript 程式碼寫得符合函數語言程式設計思想和規範的專案都較少,從而也限制了 Currying 等技術在 JavaScript 程式碼中的普遍使用。
假如我們還沒有準備好去寫函數語言程式設計規範的程式碼,僅需要在 JSX 程式碼中提前繫結一次引數,那麼 bind 或箭頭函式就足夠了。
結論
- Currying 在 JavaScript 中是“低效能”的,但是這些效能在絕大多數場景,是可以忽略的。
- Currying 的思想極大地助於提升函式的複用性。
- Currying 生於函數語言程式設計,也陷於函數語言程式設計。假如沒有準備好寫純正的函式式程式碼,那麼 Currying 有更好的替代品。
- 函數語言程式設計及其思想,是值得關注、學習和應用的事物。所以在文末再次安利 JavaScript 程式設計師閱讀此書 —— 《mostly-adequate-guide》