JavaScriptでカリー化。ありがち、つうか実際にあるでしょうね。小ネタと思ってやりはじめたら、意外と混乱した。一種のメタプログラミングのはずだが、実際にはテキスト加工処理。
内容:
- カリー化ってなに?
- カリー化を行う関数を作る:準備
- カリー化を行う関数を作る:テキストのパッチワーク
- カリー化を行う関数を作る:組み立て
●カリー化ってなに?
2引数の関数f(x, y)に対して、「gがfのカリー化」だとは、f(x, y) = g(x)(y) が常に成立すること -- ゴチャゴチャ説明するより実例実例:
functio sum(x, y) {
return x + y;
}
このsumのカリー化の例:
function curried_sum(x) {
return function (y) {return sum(x, y);}
}
curreid_sum関数は1引数で、戻り値として関数(これも1引数)を返します。実行してみると:
js> var x = curried_sum(10)
js> x(15)
25
js> curried_sum(10)(15)
25
js> curried_sum(13)(-20)
-7
js> var f = sum; var g = curried_sum
js> f(123, 999) == g(123)(999)
true
js>
最初に出したカリー化の定義がなんとなくはつかめたでしょう。もとの関数の引数が2つ以上あっても同様です。
functio mean3(a, b, c) {
return (a + b + c)/3;
}function curried_mean3(a) {
return function (b, c) {return mean3(a, b, c);}
}
js> var y = curried_mean3(10)
js> y(20, 30)
20
js> mean3(10, 20, 30) == curried_mean3(10)(20, 30)
true
js>
●カリー化を行う関数を作る:準備
sum → curried_sum、mean3 → curried_mean3は人の頭と手で行ったのですが、これを関数にやらせましょう。つまり、var curried_sum = curry(sum); var curried_mean3 = curry(mean3);
として自動的にカリー化を作り出す関数curryを定義するのです。
これを行うためには、もとの関数を加工して新しい関数を作り出すことになります。小手調べに、与えられた関数の第1引数、残りの引数、関数定義本体を、テキストとして抜き出す関数を作ってみます。
function decompFun(fun) {
if (typeof fun != 'function') {
throw new Error("The argument must be a function.");
}
if (fun.arity == 0) {
throw new Error("The function must have more than one argument.");
}var funText = fun.toString();
var args = /function .*\((.*)\)(.*)/.exec(funText)[1].split(', ');
var firstArg = args.shift();
var restArgs = args.join(', ');
var body = funText.replace(/function .*\(.*\) /, "");print("firstArg:" + firstArg);
print("restArgs:" + restArgs);
print("body:" + body);
}
js> decompFun(sum)
firstArg:x
restArgs:y
body:
{
return x + y;
}js> decompFun(mean3)
firstArg:x
restArgs:y, z
body:
{
return (x + y + z) / 3;
}js>
こうして取り出した第1引数、残りの引数、本体をつぎはぎして、新しい関数を定義するテキストを作ればいいわけです。
●カリー化を行う関数を作る:テキストのパッチワーク
具体例と共に考えるため、sumのカリー化をもう一度出します。
function (x) {
return function (y) {return x + y;}
}
これは最初のcurried_sumとはちょっと変更点があります。
- function宣言文ではなくて、function式として関数を直接表現している。
- sumという関数名の代わりにsumの定義本体を埋め込んでいる。
これを眺めれば、カリー化の一般形が予測できますね。
function (<第1引数>) {
return function (<残り引数>) <本体>
}
このパターンに従って、文字列連結で組み立てましょう。
var curriedText =
"function (" + firstArg + ") {" +
"return function (" + restArgs + ")" + body +
"}";
こうして作ったcurriedTextはあくまでテキストです。このテキストに対応する本物の関数を作って戻すのがcurryの仕事です。
●カリー化を行う関数を作る:組み立て
今まで準備した素材を使ってcurry関数全体を定義しましょう。テキストから関数を作るにはevalを使います。
function curry(fun) {
if (typeof fun != 'function') {
throw new Error("The argument must be a function.");
}
if (fun.arity == 0) {
throw new Error("The function must have more than one argument.");
}var funText = fun.toString();
var args = /function .*\((.*)\)(.*)/.exec(funText)[1].split(', ');
var firstArg = args.shift();
var restArgs = args.join(', ');
var body = funText.replace(/function .*\(.*\) /, "");var curriedText =
"function (" + firstArg + ") {" +
"return function (" + restArgs + ")" + body +
"}";eval("var curried =" + curriedText);
return curried;
}
うーん、テキストつぎはぎは、やっぱダサイな -- いいんか? これで。
js> curry(sum)(10)(15)
25
js> curry(mean3)(10)(20, 30)
20
js>
curry(curry(sum))(10)()(20)
は動きますが、JavaScriptのスコーピングの都合で curry(curry(mean3)(10))(20)(30)
はうまく動かないようです。
[追記 date="12-14"]「もっとかしこいカリー化」も見てね。[/追記]
[追記 date="12-15"]理屈も好きなかたは、「カリーをもっと -- ラムダで考えるカリー化」もどうぞ。[/追記]