JavaScript函數柯里化的一些思考


1. 高階函數的坑

在學習柯里化以前,咱們首先來看下面一段代碼:javascript

var f1 = function(x){
    return f(x);
};
f1(x);

不少同窗都能看出來,這些寫是很是傻的,由於函數f1f是等效的,咱們直接令var f1 = f;就好了,徹底沒有必要包裹那麼一層。html

可是,下面一段代碼就未必可以看得出問題來了:java

var getServerStuff = function(callback){
  return ajaxCall(function(json){
    return callback(json);
  });
};

這是我摘自《JS函數式編程指南》中的一段代碼,實際上,利用上面的規則,咱們能夠得出callback與函數git

function(json){return callback(json);};

是等價的,因此函數能夠化簡爲:ajax

var getServerStuff = function(callback){
  return ajaxCall(callback);
};

繼續化簡:正則表達式

var getServerStuff = ajaxCall;

如此一來,咱們發現那麼長一段程序都白寫了。
函數既能夠當參數,又能夠當返回值,是高階函數的一個重要特性,可是稍不留神就容易踩到坑裏。編程

2. 函數柯里化(curry)

言歸正傳,什麼是函數柯里化?函數柯里化(curry)就是隻傳遞給函數一部分參數來調用它,讓它返回一個函數去處理剩下的參數。聽得很繞口,其實很簡單,其實就是將函數的變量拆分開來調用:f(x,y,z) -> f(x)(y)(z)json

對於最開始的例子,按照以下實現,要傳入兩個參數,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雖然是一個新函數,可是f2f是徹底等效的,繞了半天,仍是繞回來了。

這裏有一個很經典的例子:

['11', '11', '11'].map(parseInt) //[ 11, NaN, 3 ]
['11', '11', '11'].map(f1(parseInt)) //[ 11, 11, 11 ]

因爲parseInt接受兩個參數,因此直接調用會有進制轉換的問題,參考"不肯相離"的文章。
var f2 = f1(parseInt)f2parseInt由原來的接受兩個參數變成了只接受一個參數的新函數,從而解決這個進制轉換問題。經過咱們的f1包裹之後就可以運行出正確的結果了。

有同窗以爲這個不算柯里化的應用,我以爲仍是算吧,各位同窗能夠一塊兒來討論下。

3. 函數柯里化進一步思考

若是說上一節的例子中,咱們不是直接運行f(x),而是把函數f當作一個參數,結果會怎樣呢?咱們來看下面這個例子:
假設f1返回函數gg的做用域指向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函數傳入的回調函數是matchmatch的主要功能是判斷每一個字符串是否匹配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'
若是這篇文章對您有幫助,請幫我點個贊,謝謝!
相關文章
相關標籤/搜索