クロージャ変換(closure.ml)

 

まだ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のクロージャ変換からやり直します。最後に、e2xが(ラベルではなく)変数として出現していなければ、クロージャの生成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は削除されます。

 

次へ進む