まだMinCamlとアセンブリの間に残っているギャップとして「ネストした関数定義」があります。これを平らにするのがクロージャ変換で、関数型言語のコンパイラではもっとも重要な処理の一つです。
「ネストした関数定義を平らにする」といっても、やさしいケースと難しいケースがあります。たとえば
let rec quad x =
let rec dbl x = x + x in
dbl (dbl x) in
quad 123
だったら
let rec dbl x = x + x in
let rec quad x = dbl (dbl x) in
quad 123
のように定義を移動するだけでOKです。しかし
let rec make_adder x =
let rec adder y = x + y in
adder in
(make_adder 3) 7
に対して同じことをしたら
let rec adder y = x + y in
let rec make_adder x = adder in
(make_adder 3) 7
のようにナンセンスなプログラムになってしまいます。これは、関数dblには自由変数がないのに対し、関数adderには自由変数xがあるためです。
このように自由変数のある関数定義を平らにするためには、adderのような関数の本体だけでなく、xのような自由変数の値も組にして扱わなければいけません。MLのコードとして書くならば、
let rec adder x y = x + y in
let rec make_adder x = (adder, x) in
let (f, fv) = make_adder 3 in
f fv 7
のような感じです。まず、関数adderは、自由変数xの値も引数として受け取るようにします。そして、関数
adderを値として扱うときは、(adder,
x) のように、関数の本体と自由変数の値の組として扱います。この組のことを関数のクロージャといいます。さらに、関数 adderを呼び出すときは、クロージャから関数の本体fと自由変数の値fvを読み出し、引数として与えます。
クロージャ変換でややこしいのは、どの関数はクロージャとして扱い、どの関数は普通に呼び出せるのか、区別する方法です。もっとも単純な方法は「すべての関数をクロージャとして扱う」ことですが、あまりにも効率が悪くなってしまいます。
そこで、MinCamlのクロージャ変換Closure.gでは、「自由変数がないとわかっていて、普通に呼び出せる」関数の集合knownを引数として受け取り、与えられた式をクロージャ変換して返します(knownと式の他に、型環境envも受け取ります)。
クロージャ変換の結果はデータ型Closure.tで表現されます。K正規形のKNormal.tとほぼ同様ですが、ネストしうる関数定義のかわりに、クロージャ生成MakeClsとトップレベル関数の集合toplevelがあります。また、一般の関数呼び出しのかわりに、クロージャによる関数呼び出しAppClsと、クロージャによらないトップレベル関数の呼び出しAppDirがあります。さらに、今後のために変数の名前のデータ型Id.tと、トップレベル関数の名前(ラベル)のデータ型Id.lを区別します。AppClsは変数を、AppDirはラベルを使用していることに注意してください。クロージャはMakeClsで変数に束縛されますが、トップレベル関数はラベルで呼び出されるためです。
Closure.gは、一般の関数呼び出しx y1 ... ynがあったら、関数xが集合knownに属してるか調べ、もし属していたらAppDirを、属していなければAppClsを返します。
let
rec x y1 ... yn =
e1 in e2のような関数定義があったら、以下の処理をします。まず、xには自由変数がないと仮定して、集合knownに追加し、本体e1をクロージャ変換してみます。そして、もし本当に自由変数がなかったら、処理を続行しe2をクロージャ変換します。もし自由変数があったら、集合knownや参照toplevelを元に戻して、e1のクロージャ変換からやり直します。最後に、e2にxが(ラベルではなく)変数として出現していなければ、クロージャの生成MakeClsを削除します。
最後の部分はちょっとわかりにくいかもしれません。たとえxに自由変数がなかったとしても、let
rec x y1 ... yn = ...
in xのように値として返すときは、クロージャを生成する必要があります。というのは、xを値として受け取る側では、自由変数があるかどうか一般にわからないので、AppDirではなくAppClsを使用して、クロージャによる関数呼び出しを行うからです。このような場合は、xが変数としてe2に出現するので、MakeClsは削除されません。一方で、let
rec x y = ... in x 123のように関数呼び出しをするだけであれば、xは(変数ではなく)AppDirのラベルとして出現するだけなので、MakeClsは削除されます。