首頁  >  文章  >  web前端  >  詳細介紹JavaScript函數柯里化的一些思考

詳細介紹JavaScript函數柯里化的一些思考

黄舟
黄舟原創
2017-03-08 14:20:031257瀏覽

1. 高階函數的坑

在學習柯里化之前,我們先來看看下面一段程式碼:

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

很多同學都能看出來,這些寫是非常愚蠢的,因為函數f1f是等效的,我們直接令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雖然是一個新函數,但是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'

以上是詳細介紹JavaScript函數柯里化的一些思考的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn