Gauche:letrec*

Gauche:letrec*

letrec*導入に向けてのメモ。

letrec*とは

R6RSで導入されたletrec*は、letrecに初期化式の実行順序の保証を入れたもの。

(letrec* ([var1 init1]
          [var2 init2]
          [var3 init3])
  body ...)

init1, init2, init3はすべてletrecと同様、 var1, var2, var3がすべて見えているスコープ内で評価されるが、 letrecと違いinit2評価時にはvar1の値が、init3評価時にはvar1, var2の値が 確定していることが保証される。たとえばこんな式が可能。

(letrec* ([var1 1] 
          [var2 (+ 1 var1)]
          [var3 (+ 1 var2)])
  var3) ;=> 3

ここにletrecを使った場合、R5RSでは結果は未定義 (処理系によってはletrec*のように 動作するものもある)、R6RSではエラー (処理系は&assertionコンディションを通知しない とだめ)。

0.8.14現在のGaucheでは上の式のletrec*をletrecに置き換えても動作する。 けれど、単純にletrec*をletrecのエイリアスにしてしまうことはできない。 letrecは最適化によって初期化式の順序が変わる場合があるからだ。

0.8.14現在の問題

未使用ローカル変数がある場合

束縛される変数が使われていない場合、その変数が除去され、対応する初期化式は (それが副作用を持たないことが証明されない限り) bodyの先頭へと移される。 次の例では、変数bが使われていないためにその除去が起こり、(print 2)が cの初期化式である(print 3)より後に実行されている。

gosh> (letrec ([a (print 1)] [b (print 2)] [c (print 3)]) (list a c))
1
3
2
(#<undef> #<undef>)

(副作用がないことが明らかな場合は、初期化式そのものも除去される)

lambda式と一般の式が混ざる場合

letrecに現れる初期化式はlambda式であることが圧倒的に多いので、 Gaucheは専用のVMインストラクションを設けている (LOCAL-ENV-CLOSURES)。 これはlambda式のコンパイル済みコードのリストをVM命令のオペランドに即値で 持っていて、1命令でローカル環境の作成、複数のクロージャの作成、 それらのローカル変数への束縛、の3ステップをやってしまうというものだ。

初期化式がlambda式でないものについては、このインストラクションの後で 式の評価とローカル変数への代入を行うインストラクション列が生成される。 したがってlambda式の評価とその他の式の評価の順序が入れ替わることがある。

クロージャの作成は(メモリアロケーションを除けば)副作用の無い操作なので、 副作用の観点からは順序の入れ替えは問題にならない。 しかし、R6RSのセマンティクスを厳密に実現しようとするなら、 初期化式の評価時にそれ以降に現れるローカル変数の値が使われたら その時点でエラーを上げなければならない。クロージャへの束縛の順序が 入れ替わっていると、未束縛であるはずのローカル変数が束縛済みに 見えてしまうためにこの検出が面倒になる (束縛状態を別に管理してもいいんだけど 面倒だしねえ)。

まあR6RSに厳密に準拠するつもりはあまりなくて、プログラムの書法上の問題で 上がるはずのエラーが上がらないって程度ならサードパーティのR6RSライブラリを 動かすって程度の用途には充分だと思うから、このissueは直さないでも 何とかなるとは思うけど。

More ...