在計算機科學中,柯里化(英語:Currying
),又譯爲卡瑞化
或加里化
,是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,而且返回接受餘下的參數並且返回結果的新函數的技術。這個技術由克里斯托弗·斯特雷奇
以邏輯學家哈斯凱爾·加里
命名的,儘管它是Moses Schönfinkel
和戈特洛布·弗雷格發明的
。javascript
在直覺上,柯里化聲稱若是你固定某些參數,你將獲得接受餘下參數的一個函數。
在理論計算機科學中,柯里化提供了在簡單的理論模型中,好比:只接受一個單一參數的lambda
演算中,研究帶有多個參數的函數的方式。
函數柯里化的對偶是Uncurrying
,一種使用匿名單參數函數來實現多參數函數的方法。java
Currying概念其實很簡單,只傳遞給函數一部分參數來調用它,讓它返回一個函數去處理剩下的參數。git
若是咱們須要實現一個求三個數之和的函數:github
function add(x, y, z) { return x + y + z; } console.log(add(1, 2, 3)); // 6
var add = function(x) { return function(y) { return function(z) { return x + y + z; } } } var addOne = add(1); var addOneAndTwo = addOne(2); var addOneAndTwoAndThree = addOneAndTwo(3); console.log(addOneAndTwoAndThree);
這裏咱們定義了一個add
函數,它接受一個參數並返回一個新的函數。調用add
以後,返回的函數就經過閉包的方式記住了add
的第一個參數。一次性地調用它實在是有點繁瑣,好在咱們可使用一個特殊的curry
幫助函數(helper function
)使這類函數的定義和調用更加容易。ajax
用ES6
的箭頭函數,咱們能夠將上面的add
實現成這樣:設計模式
const add = x => y => z => x + y + z;
好像使用箭頭函數更清晰了許多。數組
來看這個函數:閉包
function ajax(url, data, callback) { // .. }
有這樣的一個場景:咱們須要對多個不一樣的接口發起HTTP
請求,有下列兩種作法:app
ajax()
函數時,傳入全局URL
常量。URL
實參的函數引用。下面咱們建立一個新函數,其內部仍然發起ajax()
請求,此外在等待接收另外兩個實參的同時,咱們手動將ajax()
第一個實參設置成你關心的API
地址。函數
對於第一種作法,咱們可能產生以下調用方式:
function ajaxTest1(data, callback) { ajax('http://www.test.com/test1', data, callback); } function ajaxTest2(data, callback) { ajax('http://www.test.com/test2', data, callback); }
對於這兩個相似的函數,咱們還能夠提取出以下的模式:
function beginTest(callback) { ajaxTest1({ data: GLOBAL_TEST_1, }, callback); }
相信您已經看到了這樣的模式:咱們在函數調用現場(function call-site
),將實參應用(apply
) 於形參。如你所見,咱們一開始僅應用了部分實參 —— 具體是將實參應用到URL
形參 —— 剩下的實參稍後再應用。
上述概念即爲偏函數的定義,偏函數一個減小函數參數個數的過程;這裏的參數個數指的是但願傳入的形參的數量。咱們經過ajaxTest1()
把原函數ajax()
的參數個數從3
個減小到了2
個。
咱們這樣定義一個partial()
函數:
function partial(fn, ...presetArgs) { return function partiallyApplied(...laterArgs) { return fn(...presetArgs, ...laterArgs); } }
partial()
函數接收fn
參數,來表示被咱們偏應用實參(partially apply
)的函數。接着,fn
形參以後,presetArgs
數組收集了後面傳入的實參,保存起來稍後使用。
咱們建立並return
了一個新的內部函數(爲了清晰明瞭,咱們把它命名爲partiallyApplied(..)
),該函數中,laterArgs
數組收集了所有實參。
使用箭頭函數,則更爲簡潔:
var partial = (fn, ...presetArgs) => (...laterArgs) => fn(...presetArgs, ...laterArgs);
使用偏函數的這種模式,咱們重構以前的代碼:
function ajax(url, data, callback) { // .. } var ajaxTest1 = partial(ajax, 'http://www.test.com/test1'); var ajaxTest2 = partial(ajax, 'http://www.test.com/test1');
再次思考beginTest()
函數,咱們使用partial()
來重構它應該怎麼作呢?
function ajax(url, data, callback) { // .. } // 版本1 var beginTest = partial(ajax, 'http://www.test.com/test1', { data: GLOBAL_TEST_1, }); // 版本2 var ajaxTest1 = partial(ajax, 'http://www.test.com/test1'); var beginTest = partial(ajaxTest1, { data: GLOBAL_TEST_1, });
相信你已經在上述例子中看到了版本2比起版本1的優點所在了,沒錯,柯里化就是:將一個帶有多個參數的函數轉換爲一次一個的函數的過程。每次調用函數時,它只接受一個參數,並返回一個函數,直到傳遞全部參數爲止。
The process of converting a function that takes multiple arguments into a function that takes them one at a time.
Each time the function is called it only accepts one argument and returns a function that takes one argument until all arguments are passed.
假設咱們已經建立了一個柯里化版本的ajax()
函數curriedAjax()
:
curriedAjax('http://www.test.com/test1') ({ data: GLOBAL_TEST_1, }) (function callback(data) { // dosomething });
咱們將三次調用分別拆解開來,這也許有助於咱們理解整個過程:
var ajaxTest1 = curriedAjax('http://www.test.com/test1'); var beginTest = ajaxTest1({ data: GLOBAL_TEST_1, }); var ajaxCallback = beginTest(function callback(data) { // dosomething });
那麼,咱們如何來實現一個自動的柯里化的函數呢?
var currying = function(fn) { var args = []; return function() { if (arguments.length === 0) { return fn.apply(this, args); // 沒傳參數時,調用這個函數 } else { [].push.apply(args, arguments); // 傳入了參數,把參數保存下來 return arguments.callee; // 返回這個函數的引用 } } }
調用上述currying()
函數:
var cost = (function() { var money = 0; return function() { for (var i = 0; i < arguments.length; i++) { money += arguments[i]; } return money; } })(); var cost = currying(cost); cost(100); // 傳入了參數,不真正求值 cost(200); // 傳入了參數,不真正求值 cost(300); // 傳入了參數,不真正求值 console.log(cost()); // 求值而且輸出600
上述函數是我以前的JavaScript設計模式與開發實踐讀書筆記之閉包與高階函數所寫的currying
版本,如今仔細思考後發現仍舊有一些問題。
咱們在使用柯里化時,要注意同時爲函數預傳的參數的狀況。
所以把上述柯里化函數更改以下:
var currying = function(fn) { var args = Array.prototype.slice.call(arguments, 1); return function() { if (arguments.length === 0) { return fn.apply(this, args); // 沒傳參數時,調用這個函數 } else { [].push.apply(args, arguments); // 傳入了參數,把參數保存下來 return arguments.callee; // 返回這個函數的引用 } } }
使用實例:
var cost = (function() { var money = 0; return function() { for (var i = 0; i < arguments.length; i++) { money += arguments[i]; } return money; } })(); var cost = currying(cost, 100); cost(200); // 傳入了參數,不真正求值 cost(300); // 傳入了參數,不真正求值 console.log(cost()); // 求值而且輸出600
你可能會以爲每次都要在最後調用一下不帶參數的cost()
函數比較麻煩,而且在cost()
函數都要使用arguments
參數不符合你的預期。咱們知道函數都有一個length
屬性,代表函數指望接受的參數個數。所以咱們能夠充分利用預傳參數的這個特色。
借鑑自mqyqingfeng:
function sub_curry(fn) { var args = [].slice.call(arguments, 1); return function() { return fn.apply(this, args.concat([].slice.call(arguments))); }; } function curry(fn, length) { length = length || fn.length; var slice = Array.prototype.slice; return function() { if (arguments.length < length) { var combined = [fn].concat(slice.call(arguments)); return curry(sub_curry.apply(this, combined), length - arguments.length); } else { return fn.apply(this, arguments); } }; }
在上述函數中,咱們在currying的返回函數中,每次把arguments.length
和fn.length
做比較,一旦arguments.length
達到了fn.length
的數量,咱們就去調用fn
(return fn.apply(this, arguments);
)
驗證:
var fn = curry(function(a, b, c) { return [a, b, c]; }); fn("a", "b", "c") // ["a", "b", "c"] fn("a", "b")("c") // ["a", "b", "c"] fn("a")("b")("c") // ["a", "b", "c"] fn("a")("b", "c") // ["a", "b", "c"]
使用柯里化,可以很方便地借用call()
或者apply()
實現bind()
方法的polyfill
。
Function.prototype.bind = Function.prototype.bind || function(context) { var me = this; var args = Array.prototype.slice.call(arguments, 1); return function() { var innerArgs = Array.prototype.slice.call(arguments); var finalArgs = args.concat(innerArgs); return me.apply(contenxt, finalArgs); } }
上述函數有的問題在於不能兼容構造函數。咱們經過判斷this指向的對象的原型屬性,來判斷這個函數是否經過new
做爲構造函數調用,來使得上述bind
方法兼容構造函數。
Function.prototype.bind() by MDN以下說到:
綁定函數適用於用new操做符 new 去構造一個由目標函數建立的新的實例。當一個綁定函數是用來構建一個值的,原來提供的 this 就會被忽略。然而, 原先提供的那些參數仍然會被前置到構造函數調用的前面。
這是基於MVC的JavaScript Web富應用開發的bind()
方法實現:
Function.prototype.bind = function(oThis) { if (typeof this !== "function") { throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); } var aArgs = Array.prototype.slice.call(arguments, 1), fToBind = this, fNOP = function() {}, fBound = function() { return fToBind.apply( this instanceof fNOP && oThis ? this : oThis || window, aArgs.concat(Array.prototype.slice.call(arguments)) ); }; fNOP.prototype = this.prototype; fBound.prototype = new fNOP(); return fBound; };
可能遇到這種狀況:拿到一個柯里化後的函數,卻想要它柯里化以前的版本,這本質上就是想將相似f(1)(2)(3)
的函數變回相似g(1,2,3)
的函數。
下面是簡單的uncurrying
的實現方式:
function uncurrying(fn) { return function(...args) { var ret = fn; for (let i = 0; i < args.length; i++) { ret = ret(args[i]); // 反覆調用currying版本的函數 } return ret; // 返回結果 }; }
注意,不要覺得uncurrying後的函數和currying以前的函數如出一轍,它們只是行爲相似!
var currying = function(fn) { var args = Array.prototype.slice.call(arguments, 1); return function() { if (arguments.length === 0) { return fn.apply(this, args); // 沒傳參數時,調用這個函數 } else { [].push.apply(args, arguments); // 傳入了參數,把參數保存下來 return arguments.callee; // 返回這個函數的引用 } } } function uncurrying(fn) { return function(...args) { var ret = fn; for (let i = 0; i < args.length; i++) { ret = ret(args[i]); // 反覆調用currying版本的函數 } return ret; // 返回結果 }; } var cost = (function() { var money = 0; return function() { for (var i = 0; i < arguments.length; i++) { money += arguments[i]; } return money; } })(); var curryingCost = currying(cost); var uncurryingCost = uncurrying(curryingCost); console.log(uncurryingCost(100, 200, 300)()); // 600
不管是柯里化仍是偏應用,咱們都能進行部分傳值,而傳統函數調用則須要預先肯定全部實參。若是你在代碼某一處只獲取了部分實參,而後在另外一處肯定另外一部分實參,這個時候柯里化和偏應用就能派上用場。
另外一個最能體現柯里化應用的的是,當函數只有一個形參時,咱們可以比較容易地組合它們(單一職責原則(Single responsibility principle)
)。所以,若是一個函數最終須要三個實參,那麼它被柯里化之後會變成須要三次調用,每次調用須要一個實參的函數。當咱們組合函數時,這種單元函數的形式會讓咱們處理起來更簡單。
概括下來,主要爲如下常見的三個用途: