什么是柯里化术幔?
官方的說法
在計算機科學中,柯里化(英語:
Currying
)北苟,又譯為卡瑞化
或加里化
闪朱,是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,并且返回接受余下的參數而且返回結果的新函數的技術捐川。這個技術由克里斯托弗·斯特雷奇以邏輯學家哈斯凱爾·加里命名的脓鹃,盡管它是Moses Sch?nfinkel和戈特洛布·弗雷格發(fā)明的。
- 在直覺上古沥,柯里化聲稱如果你固定某些參數瘸右,你將得到接受余下參數的一個函數。
- 在理論計算機科學中岩齿,柯里化提供了在簡單的理論模型中太颤,比如:只接受一個單一參數的
lambda
演算中,研究帶有多個參數的函數的方式盹沈。 - 函數柯里化的對偶是
Uncurrying
龄章,一種使用匿名單參數函數來實現多參數函數的方法。
方便的理解
Currying
概念其實很簡單,只傳遞給函數一部分參數來調用它瓦堵,讓它返回一個函數去處理剩下的參數基协。
如果我們需要實現一個求三個數之和的函數:
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)使這類函數的定義和調用更加容易杂穷。
用ES6
的箭頭函數,我們可以將上面的add
實現成這樣:
const add = x => y => z => x + y + z;
好像使用箭頭函數更清晰了許多卦绣。
偏函數耐量?
來看這個函數:
function ajax(url, data, callback) {
// ..
}
有這樣的一個場景:我們需要對多個不同的接口發(fā)起HTTP
請求,有下列兩種做法:
- 在調用
ajax()
函數時滤港,傳入全局URL
常量廊蜒。 - 創(chuàng)建一個已經預設
URL
實參的函數引用。
下面我們創(chuàng)建一個新函數溅漾,其內部仍然發(fā)起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
數組收集了后面?zhèn)魅氲膶崊⒔旁4嫫饋砩院笫褂谩?/p>我們創(chuàng)建并
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的優(yōu)勢所在了,沒錯秧了,柯里化就是:將一個帶有多個參數的函數轉換為一次一個的函數的過程跨扮。每次調用函數時,它只接受一個參數验毡,并返回一個函數衡创,直到傳遞所有參數為止。
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.
假設我們已經創(chuàng)建了一個柯里化版本的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
我們在使用柯里化時,要注意同時為函數預傳的參數的情況动看。
因此把上述柯里化函數更改如下:
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
屬性,表明函數期望接受的參數個數奶甘。因此我們可以充分利用預傳參數的這個特點篷店。
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"]
bind方法的實現
使用柯里化钉赁,能夠很方便地借用
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
方法兼容構造函數讳苦。綁定函數適用于用
new
操作符new
去構造一個由目標函數創(chuàng)建的新的實例。當一個綁定函數是用來構建一個值的吩谦,原來提供的this
就會被忽略鸳谜。然而, 原先提供的那些參數仍然會被前置到構造函數調用的前面。
這是基于MVC
的JavaScript Web
富應用開發(fā)的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() {},
var 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;
};
反柯里化(uncurrying)
可能遇到這種情況:拿到一個柯里化后的函數式廷,卻想要它柯里化之前的版本咐扭,這本質上就是想將類似
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
柯里化或偏函數有什么用?
無論是柯里化還是偏應用妻导,我們都能進行部分傳值逛绵,而傳統(tǒng)函數調用則需要預先確定所有實參。如果你在代碼某一處只獲取了部分實參倔韭,然后在另一處確定另一部分實參术浪,這個時候柯里化和偏應用就能派上用場。
另一個最能體現柯里化應用的的是寿酌,當函數只有一個形參時胰苏,我們能夠比較容易地組合它們(
單一職責原則(Single responsibility principle)
)。因此醇疼,如果一個函數最終需要三個實參硕并,那么它被柯里化以后會變成需要三次調用,每次調用需要一個實參的函數秧荆。當我們組合函數時倔毙,這種單元函數的形式會讓我們處理起來更簡單。
歸納下來乙濒,主要為以下常見的三個用途:
- 延遲計算
- 參數復用
- 動態(tài)生成函數