JavaScript でカリー化、再び2008年02月14日 05時04分

以前、「JavaScript で引数束縛」において関数のカリー化を試みました。しかし、そこでカリー化された関数は、そのままでは一度しか部分適用ができず、また、最初の関数呼び出しは必ず部分適用として扱われていました。

function mean3(a, b, c) { return (a + b + c) / 3; }
// 「JavaScript で引数束縛」における curry 関数。
var curriedMean3 = curry(mean3);
curriedMean3(1)(2, 3); // => 2

curriedMean3(1)(2)(3);
// => TypeError: curriedMean3(1)(2) is not a function
// そのままでは部分適用を 2 回以上行えない。
// curry(curriedMean3(1))(2)(3) なら大丈夫。

curriedMean3(1, 2, 3);
// => function () { ... }
// 十分な数の引数が与えられているにもかかわらず、
// 部分適用とみなされて関数が返る。

そこで、この制限を取り除いた――すなわち、1 回カリー化するだけで複数回の部分適用が行え、十分な数の引数が与えられれば直ちに結果を返すような関数を返す――カリー化関数を作ってみようと思います。

このカリー化関数の名前を curry としましょう。curry 関数は、引数として関数を受け取り、その関数をカリー化した関数を返します。とりあえず、関数を受け取り関数を返すというのをコードにしてみます。

function curry(f) {
  return function () {
    ...
  };
}

返される関数の中では何を行えばいいのでしょうか。それはこの関数が呼び出されたときの引数の数によって異なってきます。十分な数の引数が与えられたなら、元の関数を呼び出し、その結果を返してやらなくてはいけません。ある関数 (Function オブジェクト) が必要とする引数の数は、その Function オブジェクトの length プロパティから得ることができるので、これを利用してやります。

function curry(f) {
  return function () {
    if (arguments.length >= f.length)
      return f.apply(null, arguments);
    ...
  };
}

これで「十分な数の引数が与えられたら直ちに結果を返す」ことは達成できました。引数の数が足りなかった場合は、部分適用がなされたものとみなして、関数を返すことにします。

function curry(f) {
  return function () {
    if (arguments.length >= f.length)
      return f.apply(null, arguments);
    return function () {
      ...
    };
  };
}

部分適用の結果として返ってくる関数とは何でしょうか。それは、足りない引数を受け取り、元々の関数を実行する関数です。これを実現するためには、部分適用された引数の値を覚えておく必要があります。

function curry(f) {
  return function () {
    if (arguments.length >= f.length)
      return f.apply(null, arguments);
    var args = Array.prototype.slice.call(arguments);
    return function () {
      return f.apply(null, args.concat(Array.prototype.slice.call(arguments)));
    };
  };
}

Array.prototype.slice.call(arguments) というのは Arguments オブジェクトを配列に変換するための決まり文句のようなものです。Array オブジェクトの slice メソッドは、配列の一部を抜き出し新たな配列として返しますが、引数を省略すると 0 番目から length - 1 番目までの要素を抜き出したもの、すなわち元の配列全体のコピーを返します。これを Arguments オブジェクトに適用することで、Arguments オブジェクト全体をコピーした配列が返ってくるというわけです。JavaScript 1.6 以降なら、Array generics により Array.slice(arguments) と書くこともできます。

しかし、このままでは相変わらず 1 回のカリー化につき 1 回の部分適用しかできません。複数回の部分適用ができるようにするためには、部分適用の結果として返ってくる関数も、部分適用ができるようにする必要があります。部分適用ができるようにするためにはどうすればいいか、そう、カリー化です。とりあえず 1 回の部分適用ができるようになるカリー化関数はできているのですから、それを再度適用してやればよいのです。

function curry(f) {
  return function () {
    if (arguments.length >= f.length)
      return f.apply(null, arguments);
    var args = Array.prototype.slice.call(arguments);
    return curry(function () {
      return f.apply(null, args.concat(Array.prototype.slice.call(arguments)));
    });
  };
}

これで大丈夫かと思いきや、まだ問題があります。カリー化を行うためには、関数が必要とする引数の数がわかっていなくてはいけません。部分適用の結果として返される関数が必要とする引数の数は、元々の関数の引数の数から部分適用された引数の数を引いたもの (curry 関数が返す関数の中では f.length - arguments.length) になるはずです。しかし、ここでは仮引数が 0 個の関数を使っているため、部分適用の結果として返される関数に対するカリー化がうまくいきません。

関数式や関数宣言を使う以上、仮引数の数を動的に変えることはできません。Function オブジェクトの length プロパティが書き込み可能ならば問題はないのですが、あいにくこのプロパティは読み込み専用です。そこで、仮引数の数を動的に変えて関数を作るために、Function コンストラクタを利用することにします。

function setParameterLength(f, n) {
  var funcs = arguments.callee.funcs;
  if (!(n in funcs)) {
    var argNames = [];
    for (var i = 0; i < n; i++)
      argNames.push("_" + i);
    funcs[n] = new Function("f",
                            "return function (" + argNames.join(", ") +
                            ") { return f.apply(this, arguments); };");
  }
  return funcs[n](f);
}
setParameterLength.funcs = [];

setParameterLength 関数は、引数として関数 f と数 n を受け取り、関数を返します。返される関数の仮引数の数は n (length プロパティの値は n) であり、実行すると f がそのまま呼び出されます。これを使うことで、部分適用の結果として返る関数にも正常にカリー化を適用できるようになります。

function curry(f) {
  return setParameterLength(function () {
    var restLength = f.length - arguments.length;
    if (restLength <= 0)
      return f.apply(null, arguments);
    var args = Array.prototype.slice.call(arguments);
    return curry(setParameterLength(function () {
      return f.apply(null, args.concat(Array.prototype.slice.call(arguments)));
    }, restLength));
  }, f.length);
}

これで完成です。それでは実際に試してみましょう。

var curriedMean3 = curry(mean3);
curriedMean3(1, 2, 3); // => 2 (引数がそろえばそのまま呼び出し)
curriedMean3(1, 2)(3); // => 2 (部分適用が可能)
curriedMean3(1)(2)(3); // => 2 (複数回の部分適用も可能)
curriedMean3()(1)()(2)()(3); // => 2
curriedMean3(1, 2)(3, 4); // => 2 (mean3 は第 4 引数を使わない)

うまくいきましたね。余分な引数を与えた場合、元々の関数にもその引数は渡されますが、この場合それは使われません。

さて、カリー化ができたのはいいですが、このようなことはきっと誰かがやっているだろうなと検索してみたところ、svendtofte.com - Curried JavaScript functions というページが見つかりました。そこに載っている考え方に沿ってカリー化関数を書くと以下のようになります。

function curry(f) {
  if (f.length == 0) return f;
  function iterate(args) {
    if (args.length >= f.length)
      return f.apply(null, args);
    return function () {
      return iterate(args.concat(Array.prototype.slice.call(arguments)));
    };
  }
  return iterate([]);
}

いきなり関数を返すのではなく、元々の関数を呼び出すか部分適用を行うかの処理を内部関数に切り出すことで、元々の関数を呼び出す部分が一箇所にまとまっています。内部で curry 関数を再度呼び出しているわけではないので、仮引数の数を調整する必要もありません。無引数の関数は部分適用をする意味がないので、その場合は渡された関数をそのまま返しています。