JavaScript函式柯里化的一些思考
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'
相關文章
- JavaScript函式柯里化JavaScript函式
- JavaScript函式柯里化的作用JavaScript函式
- [譯] JavaScript中的函式柯里化JavaScript函式
- JavaScript函式柯里化詳解JavaScript函式
- JavaScript進階之函式柯里化JavaScript函式
- 函式柯里化函式
- JS:函式柯里化JS函式
- Js函式柯里化JS函式
- 深入理解javascript系列(十七):函式柯里化JavaScript函式
- 前端戰五渣學JavaScript——函式柯里化前端JavaScript函式
- 函式的合成與柯里化函式
- 高階函式應用 —— 柯里化與反柯里化函式
- JS高階函式-函式柯里化JS函式
- JavaScript柯里化JavaScript
- js柯里化函式的好處JS函式
- JavaScript中的事件迴圈機制跟函式柯里化JavaScript事件函式
- 前端之函式柯里化Currying前端函式
- 函式柯里化和偏函式應用函式
- JS中的 偏函式 和 柯里化JS函式
- [譯] 柯里化與函式組合函式
- JS 分步實現柯里化函式JS函式
- JS專題之函式柯里化JS函式
- javascript中bind繫結接收者與函式柯里化JavaScript函式
- JavaScript函數語言程式設計(純函式、柯里化以及組合函式)JavaScript函數程式設計函式
- 「譯」理解JavaScript的柯里化JavaScript
- JavaScript 中的函數語言程式設計:函式,組合和柯里化JavaScript函數程式設計函式
- JavaScript 函數語言程式設計---柯里化JavaScript函數程式設計
- 用大白話介紹柯里化函式函式
- 【譯】理解JavaScript中的柯里化JavaScript
- 柯里化與反柯里化
- Javascript currying柯里化詳解JavaScript
- 「前端進階」徹底弄懂函式柯里化前端函式
- 手寫系列:call、apply、bind、函式柯里化APP函式
- 「前端面試題系列6」理解函式的柯里化前端面試題函式
- 打造屬於自己的underscore系列(五)- 偏函式和函式柯里化函式
- JavaScript 函數語言程式設計技巧 - 反柯里化JavaScript函數程式設計
- 瞭解 JavaScript 函數語言程式設計 - 柯里化JavaScript函數程式設計
- 一段柯里化函式程式碼閱讀函式
- 深入 call、apply、bind、箭頭函式以及柯里化APP函式