JavaScript で引数束縛2005年12月13日 18時14分

引数束縛 (カリー化) の話。まずは「JavaScriptでカリー化」(檜山正幸のキマイラ飼育記)。タイトルを見てどこかで聞いたような話だなと思ったら「関数の変形」(Effecttive JavaScript - Dynamic Scripting) だった。だが、前者は文字列に戻してから評価というのが力技っぽくて個人的に好きでないし、後者は汎用的過ぎていささかわかりにくい。そこで今回は先頭の引数から束縛していくというのに的を絞ってみたいと思う。

まず第 1 引数のみを束縛する場合。Function#apply を使えば引数を配列として渡せるので、束縛された値と後から渡された引数とを連結してやればいいのではないか。

function curry(func)
{
  return function (first) {
    return function () {
      var args = Array.prototype.concat.apply([first], arguments);
      return func.apply(this, args);
    };
  };
}

function sum(x, y) { return x + y; }
function mean3(a, b, c) { return (a + b + c) / 3; }

alert( curry(sum)(10)(15) );               // 25
alert( curry(mean3)(10)(20, 30) );         // 20
alert( curry(curry1(sum))(10)()(20) );     // 30
alert( curry(curry1(mean3)(10))(20)(30) ); // 20

それなりにいい感じである。檜山さんが動かないといっていた最後のパターンもちゃんと計算できているようだ。では調子に乗って束縛する引数の数を可変にしてみよう。

function curry(func)
{
  return function () {
    var length = arguments.length;
    var heads = new Array(length);
    for (var i = 0; i < length; i++)
      heads[i] = arguments[i];

    return function () {
      var args = Array.prototype.concat.apply(heads, arguments);
      return func.apply(this, args);
    };
  };
}

alert( curry(sum)(10, 20)() );    // 30
alert( curry(mean3)(5, 15)(10) ); // 10

これでうまくいくと思ったら落とし穴が。引数に配列があるとそれを展開してしまうのだ。

alert( sum(1, [2, 4]) );        // 12,4
alert( curry(sum)(1)([2, 4]) ); // 3
// sum は二つしか引数をとらないので最後の 4 は無視されている。

そこで後から受け取る引数も配列に変換することに。

function curry(func)
{
  return function () {
    var length = arguments.length;
    var heads = new Array(length);
    for (var i = 0; i < length; i++)
      heads[i] = arguments[i];

    return function () {
      var length = arguments.length;
      var tails = new Array(length);
      for (var i = 0; i < length; i++)
        tails[i] = arguments[i];

      var args = heads.concat(tails);
      return func.apply(this, args);
    };
  };
}

alert( sum(1, [2, 4]) );        // 12,4
alert( curry(sum)(1)([2, 4]) ); // 12,4

さすがにコードが長くなってきたので似たような処理をまとめる。prototype.js にも同名のメソッドがあるが Array.from を定義。

Array.from = function (array) {
  var length = array.length;
  var result = new Array(length);
  for (var i = 0; i < length; i++)
    result[i] = array[i];
  return result;
};

function curry(func)
{
  return function () {
    var heads = Array.from(arguments);
    return function () {
      var tails = Array.from(arguments);
      var args = heads.concat(tails);
      return func.apply(this, args);
    };
  };
}

これでかなりすっきりした。カリー化された関数に対して、複数回の部分適用ができるようにしたものを「JavaScript でカリー化、再び」に書いた。ついでに Function.prototype に結びつければ個人的にはさらにすっきり。以下で定義してある curry メソッドがしていることは、その名に反してカリー化ではなく部分適用である。

Function.prototype.curry = function () {
  var heads = Array.from(arguments);
  var self = this;
  return function () {
    return self.apply(this, heads.concat(Array.from(arguments)));
  };
};

alert( sum.curry(10)(15) );             // 25
alert( mean3.curry(10)(20, 30) );       // 20
alert( sum.curry(10).curry()(20) );     // 30
alert( mean3.curry(10).curry(20)(30) ); // 20
alert( sum.curry(10, 20)() );           // 30

ななしさんからのヒントにより Array.from を使わなくてもよくなった。

Function.prototype.curry = function () {
  var args = arguments;
  var self = this;
  return function () {
    Array.prototype.push.apply(args, arguments);
    return self.apply(this, args);
  };
};

ななしさんnak2k さんのコメントにより Array.from を使わなくてもよくなった。

Function.prototype.curry = function () {
  var args = arguments;
  var self = this;
  return function () {
    Array.prototype.unshift.apply(arguments, args);
    return self.apply(this, arguments);
  };
};