型推論を実装・改善していく4 状態を持つ型変数生成
前回書いた通り、TaPL準拠の型推論が完全に純粋関数型なのに対して、「ユニークな型変数を作る」new_tvar
関数に状態を持たせることでいちいち引数・返り値のタプルの一部として渡し回さなくてもいいようにする。
HaskellだったらStateモナドの出番なのだろうが、OCamlなので普通に破壊的代入でやってしまう。
今回のコード:
変更を加えたのはnew_tvar
関数そのもの、大元のtypeof
関数、そしてinfer
関数の三箇所。unify
やapplysubst
などは元々new_tvar
を触っていなかったので変更なし。
new_tvar
before
type new_tvar = NewTVar of ty * (unit -> new_tvar) let new_tvar = let rec f n () = NewTVar (TVar n, f (n+1)) in f 0
after
let new_tvar = let i = ref 0 in let f () = incr i; TVar !i in f
元々はnew_tvar
を実行すると新しい型変数と新しいnew_tvar
関数が返ってくるようになっていた。次に型変数を作るときはこの新しいnew_tvar
を使うことで作成される型変数のユニークさを担保していた。
新しいコードではただ単にミュータブルなrefを含むクロージャを作り、その関数をnew_tvar
として扱っている。new_tvar
が呼ばれるたびにこのクロージャのrefが更新される形。
typeof
before
let typeof env new_tvar constr e = let (t, new_tvar, constr1) = infer env new_tvar e in let constr = unify (constr @ constr1) in let t = applysubst constr t in (t, new_tvar, constr)
after
let typeof env constr e = let (t, constr1) = infer env e in let constr = unify (constr @ constr1) in let t = applysubst constr t in (t, constr)
typeof
関数の引数と返り値からnew_tvar
が消えている。さらに関数内部の実装を見るとinfer
の引数と返り値も同じくnew_tvar
が含まれていたのが消えている。
infer
infer
は長めなのでパターンマッチのケースごとに見ていく。
EVar
ケース
before
let rec infer env new_tvar = function | EVar s -> (List.assoc s env, new_tvar, [])
after
let rec infer env = function | EVar s -> (List.assoc s env, [])
EVar
のケースは単純なのであまり変わらない。返り値にnew_tvar
が入っているかどうかの違いで、返り値自体の複雑さもないしnew_tvar
が更新されるような場面もない。
EAbs
ケース
before
| EAbs(sparam, fbody) -> let NewTVar(tparam,new_tvar) = new_tvar () in let (tret, new_tvar, constr) = infer ((sparam,tparam)::env) new_tvar fbody in (TArrow(tparam,tret), new_tvar, constr)
after
| EAbs(sparam, fbody) -> let tparam = new_tvar () in let (tret, constr) = infer ((sparam,tparam)::env) fbody in (TArrow(tparam,tret), constr)
かなりスッキリしてきた。new_tvar
を実行すると返ってくるのが新しい型変数のみだったり、再帰的なinfer
で式の型と制約を算出したり、と実際の型推論のロジックだけがコードに出てきている印象。やはり各行に1、2回new_tvar
が出てくるのは処理の見通しをかなり悪化させる。
EApp
ケース
before
| EApp(func, arg) -> let (tfunc,new_tvar,constr1) = infer env new_tvar func in let (targ,new_tvar,constr2) = infer env new_tvar arg in let NewTVar(tret,new_tvar) = new_tvar () in let newconstr = [tfunc,TArrow(targ,tret)] in (tret, new_tvar, List.concat [newconstr; constr1; constr2])
after
| EApp(func, arg) -> let (tfunc,constr1) = infer env func in let (targ,constr2) = infer env arg in let tret = new_tvar () in let newconstr = [tfunc,TArrow(targ,tret)] in (tret, List.concat [newconstr; constr1; constr2])
私の脳のキャパシティの問題かもしれないが、各行に二回出ていたnew_tvar
が消えると一気にコードのロジックがはっきりしてくる。各行を言葉で説明するのもいちいちnew_tvar
に言及しなくてはいけないと非常にまどろっこしい。
この変更で効率化やロジックの大幅な簡略化などはできていないのだが、それでも「何が起きているか」がはっきり分かりやすくなったと感じる。
次回
今回の変更は少し前座的なところがあり(というかコメントも「スッキリしてきた」「はっきりしてきた」しか言ってない気がする)、段階的に破壊的代入の利用を導入していく過程の一部だ。その本丸は「型変数をミュータブルにすることでの単一化処理の効率化」である。次回はその変更を見ていく。