型推論を実装・改善していく6 ミュータブルな型変数を使った単相型推論(後編)
前回に続いてミュータブルな型変数に対する破壊的代入で実装する効率的な単相型推論について見ていく。今回はtypeof
関数の内部で使われているmatch_fun_ty
、そしてunify
, occursin
の実装について。
match_fun_ty
前回見たとおり、型推論・型検査を行うtypeof
関数の「関数適用」ケースでmatch_fun_ty
というヘルパ関数が使われている:
let rec typeof env = function ... | EApp(func, arg) -> let tfunc = typeof env func in let targ = typeof env arg in match_fun_ty tfunc targ
match_fun_ty
が呼ばれる前にtfunc
とtarg
、つまり関数部分と引数部分それぞれの型を再帰的なtypeof
呼び出しで算出した上で、それらを引数としてmatch_fun_ty
に与えている。match_fun_ty
の役割は、これらに含まれる型変数を単一化しつつ最終的な「関数適用の結果の型」を返すことだ。
というわけでmatch_fun_ty
の実装:
let rec match_fun_ty tfunc targ = match tfunc with | TArrow(tparam,tret) -> unify tparam targ; tret | TVar {contents=Link ty} -> match_fun_ty ty targ | TVar ({contents=Unbound _} as tvar) -> let tparam = new_tvar () in let tret = new_tvar () in tvar := Link(TArrow(tparam,tret)); unify tparam targ; tret
tfunc
に対してパターンマッチしている。個別のケースを見ていく。
| TArrow(tparam,tret) -> unify tparam targ; tret
tfunc
が関数型だった場合、その関数型の仮引数部分の型tparam
と「実引数の型」targ
をunify
関数で単一化し、それに成功したなら関数型の返り値部分の型を「関数適用の結果の型」として返す。(unify
については後述)
| TVar {contents=Link ty} -> match_fun_ty ty targ
tfunc
がすでに他の型に単一化されている型変数の場合、単一化された対象の型へのリンクを辿って、その型をtfunc
だと考えて再帰的にmatch_fun_ty
を呼び出す。
| TVar ({contents=Unbound _} as tvar) -> let tparam = new_tvar () in let tret = new_tvar () in tvar := Link(TArrow(tparam,tret)); unify tparam targ; tret
tfunc
が単一化されていない型変数の場合、新しい型変数をそれぞれ仮引数と返り値の型として作成し、それらを組み合わせた関数型を作成。tfunc
はその新しい関数型にリンクするように破壊的変更し、仮引数と実引数の型をunify
で単一化し、返り値の型であるtret
を「関数適用の結果の型」として返す。
unify
単一化関数unify
は二つの型が同値であると扱えるかを調べ、扱えるなら正しく型変数をリンクに更新してunitを返す。扱えないならエラー。
let rec unify t1 t2 = match t1, t2 with | _, _ when t1 = t2 -> ()
すでにまったく同じ型であるなら何もしない。
| TArrow(tparam1, tret1), TArrow(tparam2, tret2) -> unify tparam1 tparam2; unify tret1 tret2
どちらも関数型なのだったら仮引数部分を単一化した後、返り値部分も単一化。
| TVar{contents=Link t1}, t2 | t1, TVar{contents=Link t2} -> unify t1 t2
片方が別の型にすでに単一化されている型変数なら、その「単一化された対象の型」ともう片方の型を再帰的に単一化。
| TVar({contents=Unbound id} as tvar), ty | ty, TVar({contents=Unbound id} as tvar) -> if occursin id ty then failwith "Unification failed due to occurs check" else tvar := Link ty
片方の型が単一化されていない型変数であれば、まずその型変数がもう片方の型の中に出てこないかをoccursin
関数で調べて(再帰的な型に対する型推論はサポートされていない)、大丈夫なら型変数の中身をもう片方の型へのリンクに更新する。
occursin
ある型変数が別の型の中に出てこないか確認するoccursin
関数。「Grow Your Own Type System」の実装では(他にもいろいろ追加で処理をしているが根本的には)「型変数がある型に登場した場合はエラー、登場しない場合はunitを返す」という実装になっている。しかし関数の名前がoccursin
なので「型変数がある型に登場した場合は真、登場しない場合は偽を返す」として、エラーを投げるのはoccursin
が使われているunify
の中でのことにしたい。
実装は例によって型に対するパターンマッチ:
let rec occursin id = function | TVar{contents=Unbound id1} -> id = id1
「別の型」が単一化されていない型変数ならidの比較。
| TVar{contents=Link t} -> occursin id t
単一化された型変数なら単一化された先の型に対して再帰的にoccursin
適用。
| TArrow(tparam, tret) -> occursin id tparam || occursin id tret
関数型なら仮引数部分と返り値部分に対して別々にoccursin
検査。どちらか片方でも含まれていれば全体も真なので||
を使って組み合わせている。
まとめ&Union Findについて
これで「ミュータブルな型変数による単一化の効率的実装」はおしまい。
この「ミュータブルな型変数に破壊的変更を加えて単一化する」というコードはアルゴリズム的にはdisjoint set forestに対するunion findだ。個別の型をノード、同値な型を集合と考えて、いかに効率的に同値な集合を算出していくか、という問題に落とし込んでいる。
その観点から言うとこの実装はまだ問題がある。一つの集合を表すtreeが非常に深くなる可能性がある点だ。型変数1が型変数2に単一化され、型変数2が型変数3に単一化され・・・と長い列ができた場合、型変数1の最終的な型を調べるのにはその長いリンクを一つ一つ辿っていく必要がある。union findだとそれをうまく処理して一つのtreeが浅く広くなるようにする(例えばリンクを辿っていく段階でそれまでの型変数をすべて保持しておいて、最終的なノードに到達したらすべての型変数のリンク先を更新する、など)。型推論でこれが実際に問題になるかはわからないが、もし今回の実装でも型推論の効率が悪かったらこのようなunion findの典型的な改善を考えるのもありだろう。
次回
次回からHindley Milner型推論のキモであるLet多相を実装する。