Currying in JavaScript

Published on:

Curry 是個很有趣的東西
詳細的介紹可以上維基查看一下: en.wikipedia.org/wiki/Currying

大致的概念是這樣的:

簡單地說, 就是將函數的某一個參數抽換之後, 可以變成另外一個函數
更極端一點, 甚至可以認為

在 Haskell 語言中, 函數原生就是這樣

sumThree x y z = x + y + z
sumTwoPlusTen = sumThree 10

sumTwoPlusTen 1 2
--> gets 13

plusNine = sumTwoPlusTen (-1)
plusNine 100
--> gets 109

老實說, 多數人寫起 Haskell 應該不會感覺很直觀
但這是一個十分有趣的玩法




切入正題, 要如何在JavaScript中使用?


首先我定義一個求兩數字和的函式來做示範

var sum = function(x, y) {
  return x + y;
};

sum(1, 2); //=> 3

那如果我們要利用上面的函數, 寫另一個函數, (10 + 參數) 呢?

var sumPlusTen = sum(10);

當然, 像這樣直接比照 Haskell 的寫法是沒用的, 你只會得到 NaN, 而不是一個 Function

會寫 JavaScript 的人應該都想得到一個簡單的解法:

var plusTen = function(x) {
  return sum(10, x);
}




當然, 我不會特別寫一篇文章來講這麼無聊的事情
讓我們改寫一下 sum, 讓他可以接收不定量參數

var sum = function() {
  var i, s = 0;
  for(i = 0; i < arguments.length; i+=1) {
    s += arguments[i];
  }
  return s;
};

sum(1, 2); //=> 3

sum(1, 2, 3); //=> 6

這樣剛剛的寫法就行不通了

不過懂得如何用 apply 的人應該也可以想出來:

var sumPlusTen = function() {
  return sum.apply(null, [10].concat(arguments));
}

把陣列 [10] 接上 arguments, 再利用 apply 傳入 sum !

當我正為自已的聰明才智沾沾自喜的時候,
直譯器打了我一巴掌:

sumPlusTen(1,2,3);
//=> '10[object Arguments]'

WTF!!!!
原來 arguments 不是一個真的陣列, 所以用 concat 是接不起來的
(我真不知道當初 JavaScript 在設計的時候是怎麼想的, 搞出這種鬼)



解法:

var sumPlusTen = function() {
  var args = Array.prototype.slice.apply(arguments);
  return sum.apply(null, [10].concat(args));
};

sumPlusTen(1,2,3); //=> 16

這是一個神奇的魔術, 把 arguments 轉換成陣列, 成功!


當然身為欲求不滿的程式設計師, 我們不能就此滿足
我們不能忍受每次要用這玩意都要做這麼麻煩的手續

希望可以有這樣一個方法:

var sumPlusTen = curry(sum, 10);
sumPlusTen(1,2,3); //=> 16


var sumPlus4Plsu5 = curry(sum, 4, 5);
sumPlus4Plsu5(1,2,3); //=> 15




那就來做吧!!

var curry = function(fun) {
  var outArgs = Array.prototype.slice.apply(arguments);
  outArgs = outArgs.slice(1); // old outArgs[0] is 'fun'

  
  return function(){
    var args = Array.prototype.slice.apply(arguments);
    return fun.apply(null, outArgs.concat(args));
  };
};

執行結果:

var sumPlusTen = curry(sum, 10);
sumPlusTen(1,2,3); //=> 16


var sumPlus4Plsu5 = curry(sum, 4, 5);
sumPlus4Plsu5(1,2,3); //=> 15

成功!!

稍微說明一下:

var curry = function(fun) {
  // 將參數列轉換為陣列

  var outArgs = Array.prototype.slice.apply(arguments);
  
  // 因為第一個參數是要被 curry 的 function, 所以把它丟掉

  outArgs = outArgs.slice(1); // old outArgs[0] is 'fun'

  
  return function(){
    // 這裡的 arguments 是 curry 後被呼叫的函式所接收的參數

    var args = Array.prototype.slice.apply(arguments);
    
    // 將我們要放進去的參數, 以及後來被呼叫時傳入的參數接起來

    return fun.apply(null, outArgs.concat(args));
  };
};

如果你不介意汙染到 Function.prototype 也可以這樣寫:

Function.prototype.curry = function() {
  var outArgs = Array.prototype.slice.apply(arguments),
      originFun = this;
      
  return function() {
    var args = Array.prototype.slice.apply(arguments);
    return originFun.apply(null, outArgs.concat(args));
  };  
};

可以得到:

var sumPlusTen = sum.curry(10);
sumPlusTen(1,2,3); //=> 16

用 CoffeeScript 可以更為簡短

Function.prototype.curry = ->
  outArgs = Array.prototype.slice.apply arguments 
  =>
    args = Array.prototype.slice.apply arguments
    @apply null, outArgs.concat(args)



創用 CC 授權條款
本著作由TeenSuu Lin製作,以創用CC 姓名標示-相同方式分享 3.0 Unported 授權條款釋出。

Comments

comments powered by Disqus