1. 高階函數的坑
在學習柯里化之前,我們首先來看下面一段代碼:
var f1 = function(x){
return f(x);
};
f1(x);
很多同學都能看出來,這些寫是非常傻的,因為函數f1
和f
是等效的,我們直接令var f1 = f;
就行了,完全沒有必要包裹那么一層。
但是,下面一段代碼就未必能夠看得出問題來了:
var getServerStuff = function(callback){
return ajaxCall(function(json){
return callback(json);
});
};
這是我摘自《JS函數式編程指南》中的一段代碼,實際上,利用上面的規則,我們可以得出callback
與函數
function(json){return callback(json);};
是等價的,所以函數可以化簡為:
var getServerStuff = function(callback){
return ajaxCall(callback);
};
繼續化簡:
var getServerStuff = ajaxCall;
如此一來,我們發現那么長一段程序都白寫了。
函數既可以當參數,又可以當返回值,是高階函數的一個重要特性,但是稍不留神就容易踩到坑里。
2. 函數柯里化(curry)
言歸正傳,什么是函數柯里化?函數柯里化(curry)就是只傳遞給函數一部分參數來調用它,讓它返回一個函數去處理剩下的參數。聽得很繞口,其實很簡單,其實就是將函數的變量拆分開來調用:f(x,y,z) -> f(x)(y)(z)
。
對於最開始的例子,按照如下實現,要傳入兩個參數,f1
調用方式是f1(f,x)
。
var f1 = function(f,x){
return f(x);
};
注意,由於f
是作為一個函數變量傳入,所以f1
變成了一個新的函數。
我們將f1
變化一下,利用閉包可以寫成如下形式,則f1
調用方式變成了f1(f)(x)
,而且得到的結果完全一樣。這就完成了f1
的柯里化。
var f1 = function(f){
return function(x){
return f(x);
}
};
var f2 = f1(f);
f2(x);
其實這個例子舉得不恰當,細心的同學可能會發現,f1
雖然是一個新函數,但是f2
和f
是完全等效的,繞了半天,還是繞回來了。
這里有一個很經典的例子:
['11', '11', '11'].map(parseInt) //[ 11, NaN, 3 ]
['11', '11', '11'].map(f1(parseInt)) //[ 11, 11, 11 ]
由於parseInt
接受兩個參數,所以直接調用會有進制轉換的問題,參考"不願相離"的文章。
var f2 = f1(parseInt)
,f2
讓parseInt
由原來的接受兩個參數變成了只接受一個參數的新函數,從而解決這個進制轉換問題。通過我們的f1
包裹以后就能夠運行出正確的結果了。
有同學覺得這個不算柯里化的應用,我覺得還是算吧,各位同學可以一起來討論下。
3. 函數柯里化進一步思考
如果說上一節的例子中,我們不是直接運行f(x)
,而是把函數f
當做一個參數,結果會怎樣呢?我們來看下面這個例子:
假設f1
返回函數g
,g
的作用域指向xs
,函數f
作為g
的參數。最終我們可以寫成如下形式:
var f1 = function(f,xs){
return g.call(xs,f);
};
實際上,用f1
來替代g.call(xxx)
的做法叫反柯里化。例如:
var forEach = function(xs,f){
return Array.prototype.forEach.call(xs,f);
};
var f = function(x){console.log(x);};
var xs = {0:'peng',1:'chen',length:2};
forEach(xs,f);
反curring就是把原來已經固定的參數或者this上下文等當作參數延遲到未來傳遞。
它能夠在很大程度上簡化函數,前提是你得習慣它。
拋開反柯里化,如果我們要柯里化f1
怎么辦?
使用閉包,我們可以寫成如下形式:
var f1 = function(f){
return function(xs){
return g.call(xs,f);
}
};
var f2 = f1(f);
f2(xs);
把f
傳入f1
中,我們就可以得到f2
這個新函數。
只傳給函數一部分參數通常也叫做局部調用(partial application),能夠大量減少樣板文件代碼(boilerplate code)。
當然,函數f1
傳入的兩個參數不一定非得包含函數+非函數,可能兩個都是函數,也可能兩個都是非函數。
我個人覺得柯里化並非是必須的,而且不熟悉的同學閱讀起來可能會遇到麻煩,但是它能幫助我們理解JS中的函數式編程,更重要的是,我們以后在閱讀類似的代碼時,不會感到陌生。知乎上羅宸同學講的挺好:
並非“柯里化”對函數式編程有意義。而是,函數式編程在把函數當作一等公民的同時,就不可避免的會產生“柯里化”這種用法。所以它並不是因為“有什么意義”才出現的。當然既然存在了,我們自然可以探討一下怎么利用這種現象。
練習:
// 通過局部調用(partial apply)移除所有參數
var filterQs = function(xs) {
return filter(function(x){ return match(/q/i, x); }, xs);
};
//這兩個函數原題沒有,是我自己加的
var filter = function(f,xs){
return xs.filter(f);
};
var match = function(what,x){
return x.match(what);
};
分析:函數filterQs
的作用是:傳入一個字符串數組,過濾出包含'q'的字符串,並組成一個新的數組返回。
我們可以通過如下步驟得到函數filterQs
:
a. filter
傳入的兩個參數,第一個是回調函數,第二個是數組,filter
主要功能是根據回調函數過濾數組。我們首先將filter
函數柯里化:
var filter = function(f){
return function (xs) {
return xs.filter(f);
}
};
b. 其次,filter
函數傳入的回調函數是match
,match
的主要功能是判斷每個字符串是否匹配what
這個正則表達式。這里我們將match
也柯里化:
var match = function(what){
return function(x){
return x.match(what);
}
};
var match2 = match(/q/i);
創建匹配函數match2
,檢查字符串中是否包含字母q。
c. 把match2
傳入filter
中,組合在一起,就形成了一個新的函數:
var filterQs = filter(match2);
var xs = ['q','test1','test2'];
filterQs(xs);
從這個示例中我們也可以體會到函數柯里化的強大。所以,柯里化還有一個重要的功能:封裝不同功能的函數,利用已有的函數組成新的函數。
4. 函數柯里化的遞歸調用
函數柯里化還有一種有趣的形式,就是函數可以在閉包中調用自己,類似於函數遞歸調用。如下所示:
function add( seed ) {
function retVal( later ) {
return add( seed + later );
}
retVal.toString = function() {
return seed;
};
return retVal;
}
console.log(add(1)(2)(3).toString()); // 6
add
函數返回閉包retVal
,在retVal
中又繼續調用add
,最終我們可以寫成add(1)(2)(3)(...)
這樣柯里化的形式。
關於這段代碼的解答,知乎上的李宏訓同學回答地很好:
每調用一次add函數,都會返回retValue函數;調用retValue函數會調用add函數,然后還是返回retValue函數,所以調用add的結果一定是返回一個retValue函數。add函數的存在意義只是為了提供閉包,這個類似的遞歸調用每次調用add都會生成一個新的閉包。
5. 函數組合(compose)
函數組合是在柯里化基礎上完成的:
var compose = function(f,g) {
return function(x) {
return f(g(x));
};
};
var f1 = compose(f,g);
f1(x);
將傳入的函數變成兩個,通過組合的方式返回一個新的函數,讓代碼從右向左運行,而不是從內向外運行。
函數組合和柯里化有一個好處就是pointfree。
pointfree 模式指的是,永遠不必說出你的數據。它的意思是說,函數無須提及將要操作的數據是什么樣的。一等公民的函數、柯里化(curry)以及組合協作起來非常有助於實現這種模式。
// 非 pointfree,因為提到了數據:name
var initials = function (name) {
return name.split(' ').map(compose(toUpperCase, head)).join('. ');
};
// pointfree
var initials = compose(join('. '), map(compose(toUpperCase, head)), split(' '));
initials("hunter stockton thompson");
// 'H. S. T'