このブログの更新は Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama

メールでのご連絡は hiyama{at}chimaira{dot}org まで。

はじめてのメールはスパムと判定されることがあります。最初は、信頼されているドメインから差し障りのない文面を送っていただけると、スパムと判定されにくいと思います。

参照用 記事

JavaScriptでカリー化

JavaScriptカリー化。ありがち、つうか実際にあるでしょうね。小ネタと思ってやりはじめたら、意外と混乱した。一種のメタプログラミングのはずだが、実際にはテキスト加工処理。

内容:

  1. カリー化ってなに?
  2. カリー化を行う関数を作る:準備
  3. カリー化を行う関数を作る:テキストのパッチワーク
  4. カリー化を行う関数を作る:組み立て

●カリー化ってなに?

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とはちょっと変更点があります。

  1. function宣言文ではなくて、function式として関数を直接表現している。
  2. 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"]理屈も好きなかたは、「カリーをもっと -- ラムダで考えるカリー化」もどうぞ。[/追記]