Hindley-Milner型推論をCで実装した話
SecHack365 2019 Advent Calendar 14日目の記事です(大遅刻).
what is this
型理論とか全く知らない人がHindley-Milner型推論器(+ REPL)をCで実装したお話.
記事に誤謬などを発見したらコメントとかDM,リプライください(@cmpl_error).
きっかけ
今年の4月からHindley-Milner型推論という型推論アルゴリズムがあるのはなんとなく知っていて,実装してみたいなーという気持ちがなんとなくありました.
そして,今年の10,11月くらいにすごいH本を買って,Hindley-Milner型推論が自分の想像していた以上に強力であることを知りました.無知な自分には,一切型注釈の無い以下のコード
let f x y = x + y
が文脈からf
がint -> int -> int
と正しく型付けされると知ったときは結構衝撃でした.
で,型推論について調べていたところBasic Polymorphic Typecheckingという多相型についての論文が読みやすくて良さげということを知りました.さらに,その論文を元にした型推論アルゴリズムのC実装を見つけたので,もうこれは実装するしかないかということになりました.
実装したもの
今回の型推論の対象となる言語(以下mini-ML)のlexer,parserと,出来た構文木を解析して式の型を推論する推論器を作成しました. リポジトリ: github.com
申し訳程度のコメントもついてます.
型と型推論について
型?
型が何をしてくれているのか,mini-MLを例に出して説明します.
まずは,mini-MLの式の定義を以下に示します.は式, は変数,は整数を表しています.
\begin{align}
e ::=\ & x \tag{変数} \\
| \ \ \ & n \tag{整数} \\
| \ \ \ & {\rm fun} \ x → e \tag{ラムダ抽象} \\
| \ \ \ & (e\ e) \tag{関数適用} \\
| \ \ \ & {\rm let} \ x = e\ {\rm in}\ e \tag{let式} \\
| \ \ \ & {\rm let\ rec} \ x = e\ {\rm in}\ e \tag{letrec式} \\
\end{align}
慣れない言葉がいっぱい出てきた人のために:
- ラムダ抽象
無名関数.fun x -> e
では,x
が仮引数,e
が返り値.例えば,fun a -> a
は受け取った値をそのまま返す関数(受け取った値をa
に束縛し,a
を返しているので). - 関数適用
関数呼び出し.(e e)
はe(e)
と読み替えてもらって問題ないです. - let式
変数束縛.let x = e in e
は,変数x
に式e
を束縛し,その変数x
のスコープはin
以降の式にのみ持つ.in
以降の式はlet式の返り値になる. - let rec式
再帰する定義が書けるlet式.例えば,フィボナッチ数列のn番目を求めるプログラムを書くにはこれを使う.
(関数<
,+
,-
,if
は事前に定義されているものとする)
let rec fibo = fun n -> (((if ((< n) 2)) n) ((+ (fibo ((- n) 2))) (fibo ((- n) 1)))) in (fibo 30)
慣れない言葉がいっぱい出てきた人のために おわり
さて,説明の為に以下の事柄は事前に定義されているものとします.
true
,false
真偽値を表す変数.+
2つの値(整数)を受け取り,それらを加算した値(整数)を返す関数.((+ 200) 300)
は200 + 300
と同じ意味.pair
2つの値を受け取り,それらをタプルにして返す関数.((pair 20) false)
はタプル(20, false)
になる.
式の定義を元に,式の例を書き下してみました.
100 true fun x -> ((+ x) 1) (fun x -> ((+ x) 1) 348) ((pair 20) false) let f = fun a -> a in (f 2141) (1500 false)
ここで出てくるのが,「構文的には合ってるけど正しく実行出来ない」式です.ここでは(1500 false)
がそれに当たります(1500
は整数であり関数ではないので,関数適用はできません).
この正しく実行できない式を,実行時ではなく,解析時に検出するために導入されたのが型です.
静的型付け言語を触ったことのある人,コンパイル時にエラーが出たことがありませんか?それはコンパイラがコンパイル時に型を解析してくれているお陰です.
正しく型付けされたプログラムは不正な動きをしないことが保証されます(型安全).
さて,mini-MLの型の定義を以下に示します.は型,は型変数(後述)を表しています.
\begin{align}
t ::=\ & \alpha \tag{型変数} \\
| \ \ \ & {\rm int} \tag{整数型} \\
| \ \ \ & {\rm bool} \tag{真偽値型} \\
| \ \ \ & t → t \tag{関数型} \\
| \ \ \ & t \times t \tag{タプル型} \\
\end{align}
関数型 は型の値を受け取って型の値を返す関数の型を表しています.例えば, はint
型の値を受け取ってbool
型の値を返す関数の型となります.
試しに,先程上げた式の例の各々に型をつけてみます.式が型を持つとき,と書きます.
型の付けようがない場合はとしました.
100 : int true : bool fun x -> ((+ x) 1) : int -> int (fun x -> ((+ x) 1) 348) : int ((pair 20) false) : int * bool let f = fun a -> a in (f 2141) : int (1500 false) : error
式の型は,型付け規則によって導出することができます.
型付け規則はこわくない
\begin{align}
{\frac{\Gamma \vdash e _ 1:t _ 1→t _ 2\ \ \ \Gamma \vdash e _ 2 : t _ 1}{\Gamma \vdash (e _ 1\ e _ 2) : t _ 2}}
\end{align}
これは型付け規則の一例です.今からこれを読んでいきます.
型環境
例えば,式
x
の型を導出しろと言われたとします.
勿論x
だけじゃ型を導出することはできません.しかし,
let x = 10 in x
のように変数x
がどのように導入されたかが書かれていれば,x
の型はint
と導出することができます.変数がどのように導入されたかは,型環境に登録します.
型環境は,変数とその型の組の集合です.
ここでは,型環境をで表します.
型環境において,式の型がであるとき,
\begin{align}
\Gamma\vdash e:t
\end{align}
と書き,「型環境の下で式は型を持つ」と読みます.
型環境に変数と型の組を追加するとき,と書きます.
型環境において変数は型を持つと 仮定するとき,と書きます.
型付け規則を読む
上から下に読みます.
型付け規則の下に,その読み方の例を乗っけています.
整数
\begin{align}
n: {\rm int} \tag{T-Int}
\end{align}
整数は型を持つ.
変数
型環境から,その変数に対応する型を導出します.
\begin{align}
{\frac{x:t \in \Gamma}{\Gamma \vdash x:t}} \tag{T-Var}
\end{align}
型環境において変数は型を持つと仮定するとき,
型環境において変数は型を持つ.
小泉進次郎構文みたいだな
ラムダ抽象
は,で引数を使うため,型環境にを新たに追加してやる必要があります.
\begin{align}
{\frac{\Gamma, x:t _ 1 \vdash e:t _ 2}{\Gamma \vdash {\rm fun}\ x → e:t _ 1 → t _ 2}} \tag{T-Abs}
\end{align}
を追加した型環境において,式が型を持つとき,
その型環境においては型を持つ.
関数適用
のは必ず関数型()でなければならず,の型はの引数の型()と一致してなければなりません.
\begin{align}
{\frac{\Gamma \vdash e _ 1:t _ 1→t _ 2\ \ \ \Gamma \vdash e _ 2 : t _ 1}{\Gamma \vdash (e _ 1\ e _ 2) : t _ 2}} \tag{T-App}
\end{align}
型環境において,式が型を持ち,式が型を持つとき,
型環境においては型を持つ.
let式
は,変数をの中で使いたいので,型環境にとその型(=の型)の組を追加してあげる必要があります.
\begin{align}
{\frac{\Gamma \vdash e _ 1:t _ 1\ \ \ \Gamma,x:t _ 1 \vdash e _ 2 : t _ 2}{\Gamma \vdash {\rm let}\ x = e _ 1\ {\rm in}\ e _ 2: t _ 2}} \tag{T-Let}
\end{align}
型環境において,式が型を持ち,を追加した型環境において式が型を持つとき,
その型環境においては型を持つ.
let rec式
は,let式とは違い変数を,の中で使える必要があります.
\begin{align}
{\frac{\Gamma, x: t _ 1 \vdash e _ 1:t _ 1\ \ \ \Gamma \vdash e _ 2 : t _ 2}{\Gamma \vdash {\rm let\ rec}\ x = e _ 1\ {\rm in}\ e _ 2: t _ 2}} \tag{T-Letrec}
\end{align}
を追加した型環境において,式が型を持ち,その型環境において式が型を持つとき,
その型環境においては型を持つ.
(本当に合ってるのか自信なくなってきた)
型推論?
型推論を使えば,型注釈が無いプログラムから,プログラムや変数の型を知ることができます.OCamlを例に出します.
# let add x y = x + y;; val add : int -> int -> int = <fun>
型注釈の無いプログラム(1行目)から,変数add
の型を知ることが出来ています(2行目).
Hindley-Milner Type System
多相型付きラムダ計算のための型システムで,これを使えば型注釈が一切無くても式から適切な型が導出できることが知られています. 多相についてはまた後で.HMはパラメトリック多相とかなんとか.
Hindley-Milner型推論
型推論はどうやって行われているか説明していきます.
ズバリ,型の連立方程式を解いています!(ドン!!!)
その方程式が解けなかったら,それは型検査をパスしてはダメなプログラムなので弾けばいいわけです.
型の連立方程式
型の連立方程式とは何なのか,簡単な例を出して説明していきます.
fun x -> ((+ x) 10)
受け取った値に10を足して返す関数です.(分かりやすい形にするとfun x -> x + 10
)
まずは,連立方程式について考えていきます.連立方程式を解くとき,後で求める現在未知の値は変数にしておきます.
型の連立方程式でも同じです.後で求める未知の型は型変数()にしておきます.
実際にやってみます.型付け規則や型環境から今わかっている事をすべて書きだし,未知の型には型変数をつけます.
+
の型はで,x
の型は未知なので型変数で置きます.
+
の型はx
の型は
(+ x)
の型は不明なので,型変数と置きます.
すると,型付け規則T-Appより+
の型はと分かります.
+
の型はx
の型は(+ x)
の型は+
の型は
すると,+
によってと分かりました.これを解いて型変数の正体を暴きます!(単一化)
+
の型はx
の型は(+ x)
の型は
解いたら次行きます.T-Intより10
の型はです.
((+ x) 10)
の型は不明なので,と置きます.すると,T-Appより(+ x)
の型はと分かります.
+
の型はx
の型は(+ x)
の型は10
の型は((+ x) 10)
の型は(+ x)
の型は
すると,(+ x)
よりとなることが分かったので,解きます.
+
の型はx
の型は(+ x)
の型は10
の型は((+ x) 10)
の型は
そして,最後はfun x -> ((+ x) 10)
です.以上の情報よりx
の型は,((+ x) 10)
の型はと分かっているので,T-Absを適用すると
+
の型はx
の型は(+ x)
の型は10
の型は((+ x) 10)
の型はfun x -> ((+ x) 10)
の型は
となります.このようにして型推論器は型注釈が無いプログラムから型を推論します.
多相性
次は,以下の式について考えてみます.
fun x -> x
受け取った値をそのまま返す関数です.これの型を推論していきます.
まず,x
の型は未知なので,型変数を置きます.
x
の型は
次はfun x -> x
です.x
の型はなので,T-Absを適用すると型はとなります.
x
の型はfun x -> x
の型は
終わりです.
型変数が残ったまま推論が終了してしまいましたが,失敗ではありません.
最終的な推論結果に型変数が残った場合,その型変数にはどんな型を入れてもOKということになります(多相性).
は,でもでもでもOKです.
「未知なのでとりあえず変数として置いておく」型変数と「どんな型を入れてもOK」な型変数との区別を付けるために,後者の型変数にはを付けてと表現したりします(型スキーム).
実装
Basic Polymorphic Typecheckingという論文で実装されている型推論のソースコードを元に実装しました.
さらに,その実装を元にした実装も参考にしました.
github.com github.com http://dysphoria.net/code/hindley-milner/HindleyMilner.scala
詳しくはソースコード読んでください.
リポジトリの各ファイルとその説明を申し訳程度に書いておきます.
- src/env.c, include/env.h
型環境に関することが書かれています - src/nongeneric.c, include/nongeneric.h
ジェネリックでない型変数(つまり,「未知なのでとりあえず変数として置いておく」型変数)に関することが書かれています - src/expr.c, include/expr.c
mini-MLの式に関することが書かれています - src/type.c, include/type.h
mini-MLの型に関することが書かれています - src/infer.c, include/infer.h
型推論のコア部分.実際にここで型推論が行われています - src/lexer.c, src/token.c, src/parser.c, include/lexer.h, include/token.h, include/parser.h
入力を解析して構文木にしてます.型推論とは直接には関係していません - src/util.c, include/util.h
C言語には可変長配列とかMapがデフォルトで無いので自分で実装したもの
詳細は力尽きました.気が向いたら追記します.
感想
C言語の話をしてねえ!ただ単純に型と型推論について自分なりの解釈を語った記事になってしまっtar...
参照したリンクとか
- https://www.fos.kuis.kyoto-u.ac.jp/~igarashi/class/isle4-11w/OCaml-meeting0908-revised.pdf
- https://www.fos.kuis.kyoto-u.ac.jp/~igarashi/class/isle4-06w/text/miniml011.html
- https://github.com/semahawk/type-inference
- https://qiita.com/reki2000/items/b7f26e65930519295355
- http://lucacardelli.name/Papers/BasicTypechecking.pdf
- http://dysphoria.net/2009/06/28/hindley-milner-type-inference-in-scala/