JavaScript學習筆記(十九) 柯里化(Curry)

柯里(Curry)

在接下來,我們會討論的主題是柯里化(currying)和部分函數應用(partial function application),在我們深入這個主題之前,讓我們
首先看看什麼是部分函數用法。

函數應用(Function Application)

在一些純函數編程語言中,一個函數不被描述爲調用(called or invoked),而是應用(applied)。
在JavaScript中,我們有相同的情況——我們能應用(apply)一個函數通過Function.prototype.apply()方法,因爲函數在JavaScript中實際上就是一個對象並且它們有自己的方法。
這裏有個函數用法的例子:
// define a function
var sayHi = function (who) {
    return "Hello" + (who ? ", " + who : "") + "!";
};
// invoke a function
sayHi(); // "Hello"
sayHi('world'); // "Hello, world!"
// apply a function
sayHi.apply(null, ["hello"]); // "Hello, hello!"
正如你看到的,無論調用(invoking)一個函數或者應用(applying)一個函數,結果都是一樣的。
 apply()接受兩個參數:第一個是在函數中是綁定到this的對象;第二個參數是一個數組(包含多個參數),會成爲在函數中可訪問的類似數組(array-like)的arguments對象。
如果第一個參數是null,那麼this將指向全局對象(global object),實際發生的就是當你調用的一個函數時,它不是一個具體對象的一個方法。

當一個函數是一個對象的方法,或者不傳遞null引用。
這裏對象作爲第一個參數傳遞給apply():
var alien = {
    sayHi: function (who) {
        return "Hello" + (who ? ", " + who : "") + "!";
    }
};
alien.sayHi('world'); // "Hello, world!"
sayHi.apply(alien, ["humans"]); // "Hello, humans!"
在這段代碼中,在 sayHi()中this指向alien。在前一個例子中this指向的是全局對象(global object)。

正如這兩個例子展示的,原來我們認爲的調用一個函數不僅僅是語法糖,等同於函數應用。

注意,除apply()之外,還有一個Function.prototype.call()方法,但它仍然僅僅是apply()之上的一個語法糖。
有時,使用語法糖更好:當你有一個只接受一個參數的函數,你可以節約創建僅僅只有一個元素的數組的工作(call可以接受多個參數):
// the second is more efficient, saves an array
sayHi.apply(alien, ["humans"]); // "Hello, humans!"
sayHi.call(alien, "humans"); // "Hello, humans!"

部分函數應用(Partial Application)

現在我們知道調用一個函數實際上就是將一組參數應用於一個函數。
那麼可以僅僅傳遞一些參數,而不是全部參數嗎?如果你正在動手處理一個數學函數,這個其實和你經常做的非常類似

你有一個add()函數,將兩個數相加:x 和 y,下面的代碼片段展示你可以如何實現,給你的x=5,y=4:
// for illustration purposes
// not valid JavaScript
// we have this function
function add(x, y) {
    return x + y;
}
// and we know the arguments
add(5, 4);
// step 1 -- substitute one argument
function add(5, y) {
    return 5 + y;
}
// step 2 -- substitute the other argument
function add(5, 4) {
    return 5 + 4;
}
在這個代碼片段中,第一步和第二步在JavaScript中是不合法的,但這是教你如何動手解決這個問題
在整個函數中,你獲得第一個參數的值,並且用已知5替代不知道的x。接着重複直到你用完所有的參數。

這個例子的第一步可以被叫做部分函數應用(partial application):我們僅僅應用了(applied)第一個參數。
當你執行一個部分函數應用,你不會得到一個結果,但你會得到另一個函數作爲替代。

接下來的代碼片段展示了一個假想(imaginary)的partialApply()方法的用法。
var add = function (x, y) {
    return x + y;
};
// full application
add.apply(null, [5, 4]); // 9
// partial application
var newadd = add.partialApply(null, [5]);
// applying an argument to the new function
newadd.apply(null, [4]); // 9
正如你看到的,部分函數應用給我們另外一個函數,可以在後面用其餘參數調用的函數。
這個實際上等價於一些add(5)(4),因爲add(5)返回一個函數,可以用(4)調用。

好了,言歸正傳。在JavaScript中默認沒有partialApply()這樣的函數和方法。
但是你可以實現它們,因爲JavaScript是動態的,足以實現它。
讓一個函數知道並且處理部分函數應用的過程被叫做柯里化(currying)。

柯里化(currying)

Currying(咖喱)和辛辣的印度菜沒有一毛錢關係;它來自一位數學家的名字Haskell Curry。Haskell編程語言也是以他的名字命名的。
柯里化是一個轉換過程——我們轉換一個函數。
Currying一個可選的名字是schönfinkelisation(柯里化),以另外一名數學家名字命名,Moses Schönfinkel,是這個轉換的最早發明者。

那麼我們如何柯里化(schönfinkelify or schönfinkelize or curry)一個函數呢?其它函數式語言可能已經在語言自身中內置了柯里化,並且所有函數默認都是柯里化的。
在JavaScript中,我們可以修改add()函數,讓它成爲一個柯里化的函數,將可以處理部分函數應用。
舉個例子:
// a curried add()
// accepts partial list of arguments
function add(x, y) {
    var oldx = x, oldy = y;
    if (typeof oldy === "undefined") { // partial
        return function (newy) {
            return oldx + newy;
        };
    }
    // full application
    return x + y;
}
// test
typeof add(5); // "function"
add(3)(4); // 7
// create and store a new function
var add2000 = add(2000);
add2000(10); // 2010
在這段代碼中,在第一次調用add()時候,會創建一個包裹作爲返回值的內部函數的閉包。這個閉包會儲存原始的 x 和 y 到私有的變量 oldx 和 oldy。
如果沒有部分函數應用,參數 x 和 y 都被傳遞了,函數處理方法就是簡單的將它們相加。
add()方法的實現比實際需要的繁瑣些,僅僅是爲了說明目的。
一個更緊湊的版本會在接下來的代碼片段中展示,沒有 oldx 和 oldy,很簡單,因爲原始的 x 默認的儲存在閉包中,我們複用了 y 作爲一個局部變量,而不是創建一個新的變量 newy,像我們在前一個例子中那樣:
// a curried add
// accepts partial list of arguments
function add(x, y) {
    if (typeof y === "undefined") { // partial
        return function (y) {
            return x + y;
        };
    }
    // full application
    return x + y;
}
在這個例子中,函數add()它自身維護部分函數應用。但是我們能實現更加的流行的泛型實現嗎?
換句話說,我們能能將任何一個函數轉換成可以接受部分函數的新函數嗎?
接下來的代碼段,將會展示一個通用函數,讓我們叫它schonfinkelize(),就會做到我們想要的。
我們使用schonfinkelize()這個名字,是因爲本身這就很難取名,並且讓它聽起來像個動詞(用curry可能有歧義),
我們需要一個動詞去表明這是個函數轉換的過程。
下面的就是通用的柯里函數:
function schonfinkelize(fn) {
    var slice = Array.prototype.slice,
        stored_args = slice.call(arguments, 1);
    return function () {
        var new_args = slice.call(arguments),
            args = stored_args.concat(new_args);
        return fn.apply(null, args);
    };
}
schonfinkelize()可能比應該的情況複雜了點,因爲arguments在JavaScript中不是一個真正的數組。
Array.prototype借用slice()方法幫我們將arguments變成一個數組,更加方便的處理。
當我們第一次調用schonfinkelize(),它會儲存一個slice()方法的引用(叫slice),還會儲存被調用時傳遞的參數(到stored_args中),只丟棄第一個參數,因爲第一參數就是將要柯里化的函數。
接着,schonfinkelize()返回一個新函數,當新函數被調用時,它會訪問(通過閉包)已經私下存儲的參數stored_argsslice引用。
新函數不得不合並先前的部分化應用的參數(stored_args)和新傳遞的參數(new_args),讓後將它們應用到原始的函數fn(在閉包中可以訪問)。

現在,裝備了讓任何函數柯里化的通用函數,我們來用幾個例子試一下:
// a normal function
function add(x, y) {
    return x + y;
}
// curry a function to get a new function
var newadd = schonfinkelize(add, 5);
newadd(4); // 9
// another option -- call the new function directly
schonfinkelize(add, 6)(7); // 13
轉換函數schonfinkelize()並不僅限於一個單獨的參數和一級的柯里化。有更多的用法:
// a normal function
function add(a, b, c, d, e) {
    return a + b + c + d + e;
}
// works with any number of arguments
schonfinkelize(add, 1, 2, 3)(5, 5); // 16
// two-step currying
var addOne = schonfinkelize(add, 1);
addOne(10, 10, 10, 10); // 41
var addSix = schonfinkelize(addOne, 2, 3);
addSix(5, 5); // 16

什麼時候使用柯里化(When to Use Currying)

當你發現你調用同一個的函數,而傳遞的參數大部分都是一樣的時候,那麼這個參數就是一個很好的可以柯里化的候選函數。
你可以動態創建一個新函數,部分應用一組參數到你的函數。接着新函數會存儲重複的參數(那麼你就不用每次都傳遞),並且會用它們去填充原始函數需要的參數列表。






發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章