檜山正幸のキマイラ飼育記 このページをアンテナに追加 RSSフィード

キマイラ・サイトは http://www.chimaira.org/です。
トラックバック/コメントは日付を気にせずにどうぞ。
連絡は hiyama{at}chimaira{dot}org へ。
蒸し返し歓迎!
ところで、アーカイブってけっこう便利ですよ。タクソノミーも作成中。今は疲れるので作っていません。

2005-12-13 (火)

JavaScriptでカリー化

| 13:07 | 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"]理屈も好きなかたは、「カリーをもっと -- ラムダで考えるカリー化」もどうぞ。[/追記]

KENZKENZ 2005/12/14 02:57 いろいろコネコネしてたらこんなのでも動きました。curry(sum)(3)(2)とかしか試してませんが…。
function curry(f) {
return function (arg1) {
return function () {
var args = [arg1];
for (var i = 0; i < arguments.length; i++) {
args.push(arguments[i]);
}
return f.apply(this, args);
};
};
}

既にご存知かもしれませんが、これ作るのに参考にしたページです:
http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference

KENZKENZ 2005/12/14 03:00 curry(curry(mean3)(10))(20)(30)
これも動きました。意外と完璧かもです。

KENZKENZ 2005/12/14 03:01 ちなみに、IE6、Mozilla Firefox 1.5で動いてるので結構どこでも動きそうです。(以上3コメントも消費しちゃってすみません)

m-hiyamam-hiyama 2005/12/14 09:15 思いついたものの、どうせ“無視されるネタ”だろうと書いてみたのですが、これ、反応よかったですね :-) 「Function.apply使えよ」と突っこまれまくりですがね。でも、さびしくないから*うれしい*。
「咳をしたら一人じゃない」(実際、いま咳している、風邪)