什么是柯里化?
官方的說法
在計(jì)算機(jī)科學(xué)中,柯里化(英語:
Currying),又譯為卡瑞化或加里化,是把接受多個(gè)參數(shù)的函數(shù)變換成接受一個(gè)單一參數(shù)(最初函數(shù)的第一個(gè)參數(shù))的函數(shù),并且返回接受余下的參數(shù)而且返回結(jié)果的新函數(shù)的技術(shù)。這個(gè)技術(shù)由克里斯托弗·斯特雷奇以邏輯學(xué)家哈斯凱爾·加里命名的,盡管它是Moses Sch?nfinkel和戈特洛布·弗雷格發(fā)明的。
- 在直覺上,柯里化聲稱如果你固定某些參數(shù),你將得到接受余下參數(shù)的一個(gè)函數(shù)。
- 在理論計(jì)算機(jī)科學(xué)中,柯里化提供了在簡單的理論模型中,比如:只接受一個(gè)單一參數(shù)的
lambda演算中,研究帶有多個(gè)參數(shù)的函數(shù)的方式。 - 函數(shù)柯里化的對(duì)偶是
Uncurrying,一種使用匿名單參數(shù)函數(shù)來實(shí)現(xiàn)多參數(shù)函數(shù)的方法。
方便的理解
Currying概念其實(shí)很簡單,只傳遞給函數(shù)一部分參數(shù)來調(diào)用它,讓它返回一個(gè)函數(shù)去處理剩下的參數(shù)。
如果我們需要實(shí)現(xiàn)一個(gè)求三個(gè)數(shù)之和的函數(shù):
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);
- 這里我們定義了一個(gè)
add函數(shù),它接受一個(gè)參數(shù)并返回一個(gè)新的函數(shù)。調(diào)用add之后,返回的函數(shù)就通過閉包的方式記住了add的第一個(gè)參數(shù)。一次性地調(diào)用它實(shí)在是有點(diǎn)繁瑣,好在我們可以使用一個(gè)特殊的curry幫助函數(shù)(helper function)使這類函數(shù)的定義和調(diào)用更加容易。
用ES6的箭頭函數(shù),我們可以將上面的add實(shí)現(xiàn)成這樣:
const add = x => y => z => x + y + z;
好像使用箭頭函數(shù)更清晰了許多。
偏函數(shù)?
來看這個(gè)函數(shù):
function ajax(url, data, callback) {
// ..
}
有這樣的一個(gè)場景:我們需要對(duì)多個(gè)不同的接口發(fā)起HTTP請(qǐng)求,有下列兩種做法:
- 在調(diào)用
ajax()函數(shù)時(shí),傳入全局URL常量。 - 創(chuàng)建一個(gè)已經(jīng)預(yù)設(shè)
URL實(shí)參的函數(shù)引用。
下面我們創(chuàng)建一個(gè)新函數(shù),其內(nèi)部仍然發(fā)起ajax()請(qǐng)求,此外在等待接收另外兩個(gè)實(shí)參的同時(shí),我們手動(dòng)將ajax()第一個(gè)實(shí)參設(shè)置成你關(guān)心的API地址。
對(duì)于第一種做法,我們可能產(chǎn)生如下調(diào)用方式:
function ajaxTest1(data, callback) {
ajax('http://www.test.com/test1', data, callback);
}
function ajaxTest2(data, callback) {
ajax('http://www.test.com/test2', data, callback);
}
對(duì)于這兩個(gè)類似的函數(shù),我們還可以提取出如下的模式:
function beginTest(callback) {
ajaxTest1({
data: GLOBAL_TEST_1,
}, callback);
}
- 相信您已經(jīng)看到了這樣的模式:我們?cè)诤瘮?shù)調(diào)用現(xiàn)場(function call-site),將實(shí)參應(yīng)用(apply) 于形參。如你所見,我們一開始僅應(yīng)用了部分實(shí)參 —— 具體是將實(shí)參應(yīng)用到
URL形參 —— 剩下的實(shí)參稍后再應(yīng)用。
上述概念即為偏函數(shù)的定義,偏函數(shù)一個(gè)減少函數(shù)參數(shù)個(gè)數(shù)的過程;這里的參數(shù)個(gè)數(shù)指的是希望傳入的形參的數(shù)量。我們通過ajaxTest1()把原函數(shù)ajax()的參數(shù)個(gè)數(shù)從3個(gè)減少到了2個(gè)。
我們這樣定義一個(gè)partial()函數(shù):
function partial(fn, ...presetArgs) {
return function partiallyApplied(...laterArgs) {
return fn(...presetArgs, ...laterArgs);
}
}
partial()函數(shù)接收fn參數(shù),來表示被我們偏應(yīng)用實(shí)參(partially apply)的函數(shù)。接著,fn形參之后,presetArgs數(shù)組收集了后面?zhèn)魅氲膶?shí)參,保存起來稍后使用。我們創(chuàng)建并
return了一個(gè)新的內(nèi)部函數(shù)(為了清晰明了,我們把它命名為partiallyApplied(..)),該函數(shù)中,laterArgs數(shù)組收集了全部實(shí)參。
使用箭頭函數(shù),則更為簡潔:
var partial = (fn, ...presetArgs) =>(...laterArgs) =>fn(...presetArgs, ...laterArgs);
使用偏函數(shù)的這種模式,我們重構(gòu)之前的代碼:
function ajax(url, data, callback) {
// ..
}
var ajaxTest1 = partial(ajax, 'http://www.test.com/test1');
var ajaxTest2 = partial(ajax, 'http://www.test.com/test1');
再次思考beginTest()函數(shù),我們使用partial()來重構(gòu)它應(yīng)該怎么做呢?
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,
});
一次傳一個(gè)
相信你已經(jīng)在上述例子中看到了版本2比起版本1的優(yōu)勢所在了,沒錯(cuò),柯里化就是:將一個(gè)帶有多個(gè)參數(shù)的函數(shù)轉(zhuǎn)換為一次一個(gè)的函數(shù)的過程。每次調(diào)用函數(shù)時(shí),它只接受一個(gè)參數(shù),并返回一個(gè)函數(shù),直到傳遞所有參數(shù)為止。
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.
假設(shè)我們已經(jīng)創(chuàng)建了一個(gè)柯里化版本的ajax()函數(shù)curriedAjax():
curriedAjax('http://www.test.com/test1')({
data: GLOBAL_TEST_1,
})
(function callback(data) {
// dosomething
});
我們將三次調(diào)用分別拆解開來,這也許有助于我們理解整個(gè)過程:
var ajaxTest1 = curriedAjax('http://www.test.com/test1');
var beginTest = ajaxTest1({
data: GLOBAL_TEST_1,
});
var ajaxCallback = beginTest(function callback(data) {
// dosomething
});
實(shí)現(xiàn)柯里化
那么,我們?nèi)绾蝸韺?shí)現(xiàn)一個(gè)自動(dòng)的柯里化的函數(shù)呢?
var currying = function(fn) {
var args = [];
return function() {
if (arguments.length === 0) {
return fn.apply(this, args); // 沒傳參數(shù)時(shí),調(diào)用這個(gè)函數(shù)
} else {
[].push.apply(args, arguments); // 傳入了參數(shù),把參數(shù)保存下來
return arguments.callee; // 返回這個(gè)函數(shù)的引用
}
}
}
調(diào)用上述currying()函數(shù):
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); // 傳入了參數(shù),不真正求值
cost(200); // 傳入了參數(shù),不真正求值
cost(300); // 傳入了參數(shù),不真正求值
console.log(cost()); // 求值并且輸出600
我們?cè)谑褂每吕锘瘯r(shí),要注意同時(shí)為函數(shù)預(yù)傳的參數(shù)的情況。
因此把上述柯里化函數(shù)更改如下:
var currying = function(fn) {
var args = Array.prototype.slice.call(arguments, 1);
return function() {
if (arguments.length === 0) {
return fn.apply(this, args); // 沒傳參數(shù)時(shí),調(diào)用這個(gè)函數(shù)
} else {
[].push.apply(args, arguments); // 傳入了參數(shù),把參數(shù)保存下來
return arguments.callee; // 返回這個(gè)函數(shù)的引用
}
}
}
使用實(shí)例:
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); // 傳入了參數(shù),不真正求值
cost(300); // 傳入了參數(shù),不真正求值
console.log(cost()); // 求值并且輸出600
你可能會(huì)覺得每次都要在最后調(diào)用一下不帶參數(shù)的cost()函數(shù)比較麻煩,并且在cost()函數(shù)都要使用arguments參數(shù)不符合你的預(yù)期。我們知道函數(shù)都有一個(gè)length屬性,表明函數(shù)期望接受的參數(shù)個(gè)數(shù)。因此我們可以充分利用預(yù)傳參數(shù)的這個(gè)特點(diǎn)。
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);
}
};
}
在上述函數(shù)中,我們?cè)赾urrying的返回函數(shù)中,每次把arguments.length和fn.length作比較,一旦arguments.length達(dá)到了fn.length的數(shù)量,我們就去調(diào)用fn(return fn.apply(this, arguments);)
驗(yàn)證:
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方法的實(shí)現(xiàn)
使用柯里化,能夠很方便地借用
call()或者apply()實(shí)現(xiàn)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);
}
}
上述函數(shù)有的問題在于不能兼容構(gòu)造函數(shù)。我們通過判斷this指向的對(duì)象的原型屬性,來判斷這個(gè)函數(shù)是否通過
new作為構(gòu)造函數(shù)調(diào)用,來使得上述bind方法兼容構(gòu)造函數(shù)。綁定函數(shù)適用于用
new操作符new去構(gòu)造一個(gè)由目標(biāo)函數(shù)創(chuàng)建的新的實(shí)例。當(dāng)一個(gè)綁定函數(shù)是用來構(gòu)建一個(gè)值的,原來提供的this就會(huì)被忽略。然而, 原先提供的那些參數(shù)仍然會(huì)被前置到構(gòu)造函數(shù)調(diào)用的前面。
這是基于MVC的JavaScript Web富應(yīng)用開發(fā)的bind()方法實(shí)現(xiàn):
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)
可能遇到這種情況:拿到一個(gè)柯里化后的函數(shù),卻想要它柯里化之前的版本,這本質(zhì)上就是想將類似
f(1)(2)(3)的函數(shù)變回類似g(1,2,3)的函數(shù)。
下面是簡單的uncurrying的實(shí)現(xiàn)方式:
function uncurrying(fn) {
return function(...args) {
var ret = fn;
for (let i = 0; i < args.length; i++) {
ret = ret(args[i]); // 反復(fù)調(diào)用currying版本的函數(shù)
}
return ret; // 返回結(jié)果
};
}
注意,不要以為uncurrying后的函數(shù)和currying之前的函數(shù)一模一樣,它們只是行為類似!
var currying = function(fn) {
var args = Array.prototype.slice.call(arguments, 1);
return function() {
if (arguments.length === 0) {
return fn.apply(this, args); // 沒傳參數(shù)時(shí),調(diào)用這個(gè)函數(shù)
} else {
[].push.apply(args, arguments); // 傳入了參數(shù),把參數(shù)保存下來
return arguments.callee; // 返回這個(gè)函數(shù)的引用
}
}
}
function uncurrying(fn) {
return function(...args) {
var ret = fn;
for (let i = 0; i < args.length; i++) {
ret = ret(args[i]); // 反復(fù)調(diào)用currying版本的函數(shù)
}
return ret; // 返回結(jié)果
};
}
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
柯里化或偏函數(shù)有什么用?
無論是柯里化還是偏應(yīng)用,我們都能進(jìn)行部分傳值,而傳統(tǒng)函數(shù)調(diào)用則需要預(yù)先確定所有實(shí)參。如果你在代碼某一處只獲取了部分實(shí)參,然后在另一處確定另一部分實(shí)參,這個(gè)時(shí)候柯里化和偏應(yīng)用就能派上用場。
另一個(gè)最能體現(xiàn)柯里化應(yīng)用的的是,當(dāng)函數(shù)只有一個(gè)形參時(shí),我們能夠比較容易地組合它們(
單一職責(zé)原則(Single responsibility principle))。因此,如果一個(gè)函數(shù)最終需要三個(gè)實(shí)參,那么它被柯里化以后會(huì)變成需要三次調(diào)用,每次調(diào)用需要一個(gè)實(shí)參的函數(shù)。當(dāng)我們組合函數(shù)時(shí),這種單元函數(shù)的形式會(huì)讓我們處理起來更簡單。
歸納下來,主要為以下常見的三個(gè)用途:
- 延遲計(jì)算
- 參數(shù)復(fù)用
- 動(dòng)態(tài)生成函數(shù)