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); }; };
コメント
_ ななし ― 2005年12月14日 16時18分
_ nanto_vi ― 2005年12月15日 14時05分
ちなみに Array#flatten は前に試したことがあります。
「多元配列を一元配列に変換」
http://nanto.asablo.jp/blog/2005/10/08/101406
_ nak2k ― 2005年12月19日 16時23分
「ななしさんからのヒントにより~」の箇所の関数なのですが、カリー化functionを生成したコンテキストにクロージングされた変数 args を Array#push で変更してしまってるために、以下のような形で意図しない動作になりませんか?
function test(a,b,c,d) { return a+b+c+d; }
var f = test.curry(1,2);
f(3,4); // --> 10, args == [1,2,3,4]
f(2,4); // --> 10, args == [1,2,3,4,2,4]
f(1,4); // --> 10, args == [1,2,3,4,2,4,1,4]
ちなみに私の場合、ArgumentsオブジェクトのArray化は、 Array.apply(null, arguments) で行うことが多いのですが、今回のケースではななしさんのArray#splice方式のほうが性能よさそうですね。(splice.call 呼び出しの引数に 0 が4つある意味が一瞬分かりにくかったですが^^;)
_ nanto_vi ― 2005年12月19日 22時43分
# ななしさんの splice に 0 が 4 つは私も最初「?」でした。
_ 仕様書係 ― 2008年12月11日 08時56分
ありがとうございます。
※メールアドレスとURLの入力は必須ではありません。 入力されたメールアドレスは記事に反映されず、ブログの管理者のみが参照できます。
※投稿には管理者が設定した質問に答える必要があります。
トラックバック
このエントリのトラックバックURL: http://nanto.asablo.jp/blog/2005/12/13/176033/tb
// JS 歴は、NN2 の終わり頃からなので、8年強です。
// ECMA262-1,2,3 はそれなりに追ってきたつもりですが、
// JavaScript-C 実装のごく一部しか読解していない程度のヘタレです。
> 引数に配列があるとそれを展開してしまうのだ。
さすがです。多くの人が手を抜いたり、そもそも、気付いていませんよね。
// Function#apply で展開された引数リストの内、
// 配列は、Array#concat で更に展開されてしまう。
// 逆に、これを利用すれば、Ruby の Array#flatten あたりは
// 簡潔に書けて真似出来ますよね。
そこで、Array#splice を使ってみました。
また、"self" が "this" を使っている場合に備えて、
"thisArg" と言う名前の引数を持てるようにしてみましたので、
function の入れ子は元に戻っています。
function Function_prototype_curry(thisArg) {
var f = this;
return function () {
var as = arguments;
Array.prototype.splice.call(as, 0, 0, 0, 0);
return function () {
Array.prototype.splice.apply(arguments, as);
return f.apply(thisArg, arguments);
};
};
}