生成コードの関数定義と関数適用
Elmでは関数がデフォルトでカリー化されており部分適用も可能ですが、JavaScriptの関数はそうではありません。
単にfunction(a) { function(b) {...といったように引数の数だけネストした関数に変換すればカリー化されたことにはなりますが、Elmコンパイラはより複雑な処理を加えています。
本稿ではElmの関数定義と関数適用がどのようなJSのコードに変換されるかを追いかけてみます。対象とするバージョンは0.19.1です。
1変数関数
1変数だとカリー化もクソもないですが見ておきましょう。id x = xをコンパイルします。
code:js
var $author$project$Main$id = function (x) {
return x;
};
変数名のプレフィックスは絶対につきますがここでは重要でないので無視しましょう。これはほぼそのまま変換されていますね。もちろん関数本体内の定義によってはそのままということにはなりませんが、ただのfunctionの定義に変換されるのは同じはずです。
2変数関数
code:elm
add: Int -> Int -> Int
add x y = x + y
ただ足し算するだけのadd関数は次のようにコンパイルされます。
code:js
var $author$project$Main$add = F2(
function (x, y) {
return x + y;
});
1変数のときと同様に逐語的に変換した関数の本体をF2に食わせたものがaddの定義です。このF2が肝で、Elmコンパイラが一緒に生成しているヘルパー関数です。定義を読みましょう。
code:js
function F(arity, fun, wrapper) {
wrapper.a = arity;
wrapper.f = fun;
return wrapper;
}
function F2(fun) {
return F(2, fun, function(a) { return function(b) { return fun(a,b); }; })
}
F2はさらにFを呼び出しているので先にそちらを見ましょう。
第一引数にはarity、つまり引数の数を食わせます。第二引数のfunは関数の本体です。F2の引数がそのまま渡されていますね。第三引数wrapperは元の関数をカリー化したものです。F2の定義を読むとわかるように、引数を一つずつ受け取り最後にfunを呼び出すようになっています。
戻り値はプロパティとしてa: arityとf: funを持たせたwrapper(つまりカリー化された関数)になっています。arityとfunは何故必要なのでしょうか。それを理解するには関数適用の生成コードを読む必要があります。
code:elm
i = add 1 2
コンパイルするとこうなります。
code:js
var $author$project$Main$i = A2($author$project$Main$add, 1, 2);
今度はA2というヘルパー関数が現れました。定義を読みましょう。
code:js
function A2(fun, a, b) {
return fun.a === 2 ? fun.f(a, b) : fun(a)(b);
}
さきほど確認したように、関数$author$project$Main$addは引数の数(a)とカリー化前の関数定義(f)をプロパティに持ったカリー化関数でした。引数の数aは2だったため、こいつをA2に食わせると呼び出されるのはfun.f、つまりカリー化前の関数です。おそらくはパフォーマンスの向上のため、引数を全部受け取った場合はカリー化前の定義を呼び出すことになっているのです。
ではaddOne = add 1というように引数を一部だけ渡すとどうなるでしょう。
code:js
var $author$project$Main$add1 = $author$project$Main$add(1);
今度はカリー化された関数にそのまま引数を食わせました。この関数はfunction(b) { return 1 + b }と等価です。
3変数関数
code:elm
joinThreeStr : String -> String -> String -> String
joinThreeStr a b c =
a ++ b ++ c
joinWithHelloWorld : String -> String
joinWithHelloWorld c =
let
f =
joinThreeStr "Hello " "World"
in
f c
ただ文字列を結合しているだけです(名付けが下手くそ)。joinWithHelloWorldは部分適用した関数をローカル変数に束縛しています。さあコンパイル!
code:js
var $author$project$Main$joinThreeStr = F3(
function (a, b, c) {
return _Utils_ap(
a,
_Utils_ap(b, c));
});
var $author$project$Main$joinWithHelloWorld = function (c) {
var f = A2($author$project$Main$joinThreeStr, 'Hello ', 'World');
return f(c);
};
二変数がF2だったんだからここはF3です。_Utils_apという関数を呼んでいますがこれは文字列結合のためのユーティリティなので無視しましょう。注目すべきは下の部分適用の例ですね。add1ではカリー化関数をそのまま呼び出していましたが、ここではA2で包まれています。A2の定義を覚えていることとして説明すると、fun.a === 2は今回はfalseになるのでfun(a)(b)が評価されます。ちゃんと部分適用できてます。
それ以上
コンパイラはF9, A9まで用意しています。引数が10個以上になってもコンパイラは怒りはしませんがカリー化しかしてくれません。
code:js
var $author$project$Main$func = function (a) {
return function (b) {
return function (c) {
return function (d) {
return function (e) {
return function (f) {
return function (g) {
return function (h) {
return function (i) {
return function (j) {
return 'hoge';
};
};
};
};
};
};
};
};
};
};
無闇に引数を増やすなよってことですね。