追記: この記事を書いてから早2年弱。今は数式のほうがわかりやすいなあと思うようになりました。
これは何?
- この記事は、最近 Hindley Milner (Algorithm W) 型推論を理解したばかりの私が、型推論のことをなるべくわかりやすく解説するものです。
- 簡略化したOCamlのようなコードが登場します。
- わかりやすさ重視で書いていくつもりなので、専門用語(自由変数とか)はあまり出てきません。
間違いなどあったらコメントしていただけると幸いです。
まずは単相から
ここからは図を使って説明していきます。(ビジュアルなほうがわかりやすいハズ)
とても簡単な例
見ればわかりますが、x
の型はint
ですね。
ですが、どのようにしてint
だとわかったのでしょうか。
すでにわかっていることを書き出しだしてみました。リテラルの右肩を見てください。
1
の型は当然ながらint
、x
の型はとりあえず0
というidをつけておきました。このidが重要な役割を果たします。
let x = y
という式があったとき、x
とy
の型は当然ながら等しくなります。
したがって、先ほどのidが0
のx
の型はint
ということになります。
以下のT(id)
はidがid
の変数の型を表しています。
T(0) = int
となりました。
print_int
の引数にx
がありますが、x
のidは0
なので、T(0) = int
よりx
の型はint
だとわかります。
関数
以下の画像を見てください。
先ほどの例よりも少し難しくなりました。先に言っておくと、f
の型はint -> int
、x
は当然ながらint
ですね。
まずは先ほどと同じようにわかることを書き出してみます。
let f x y = x + y
という式があったとき、f
の型はxの型 -> yの型 -> x + yの型
と等しくなります。
let式の中身を見てみると、x + 1
とあります。これはx
の型は1の型、すなわちint
と等しいということになります。
x + 1
の型はint
だとわかりました。するとf
の型はint -> int
だとわかります。
xの型 -> x + 1の型
ということですね
関数2
letの式の中身を評価していきます。
g
の中のx + y
はint
。
よってg
はxの型 -> x + yの型
でint -> int
。
f
の中はlet g x = x + y in g x
ですが、結局はg x
です。
g
はint -> int
なのでg
の引数のx
はint
、g x
もint
とわかります。
結果的にf
の型はxの型 -> yの型 -> g xの型
でint -> int -> int
とわかります。
以上、単相型推論の簡単な解説でした。
そして多相へ
さて、多相になるとちょっと難しくなります。
一見正しく型推論できているようですが、
このままだとf 1 2
を評価したときに T(0)
がint -> int -> bool
となってしまい、以降f
が呼び出された時に多相性が失われてしまいます。f
が'a -> 'a -> bool
だったはずなのに、'a
がint
で上書きされてしまいます。
f 1.2 3.4
などとすると型エラーになるわけです。これではいけません。
ここからは多相型推論の手順を説明します。
下の図のT(2) -> bool
の右下をみてください。
何か{}
という物がついていますね。ここにはその型に含まれるidから, その型環境に含まれるすべての型のそれぞれに含まれるidからそのそれぞれの型の{}に含まれるidを取り除いたもの, を取り除いたものが含まれます。
... 多分今の説明で理解できた人は少ないと思います ...
ここからは、多相型推論を理解する上で知っておかなくてはならないことをまとめます。
まず型環境というのは、簡単に言えば、ある型を推論する時点ですでに存在が知られている型の集合です。
上の図で3行目までを読み込んでg
の型を推論する場合なら、f
, x [id=1のやつ]
, y
がそうです。g
の型を推論するのですから、g
やx [id=4のやつ]
は含まれません。
そして肝心の {}
の役割ですが、ある{}
を持つ型は、instantiateされるときにその{}
に含まれるidと同じidの型を新しいidの型に変えます。instantiateというのは、ある変数が使われるときに、型の多相性を実現するために、その変数の型に今説明した手順を実行することです。図に示すと次のようになります:
T(0) -> T(0) -> T(0)
という型があったとして、{0}
がくっついているなら、その中のT(0)
を全て新しいT(1)
で置換します。T(1)
というのは、まだ使われていないidならなんでも良いはずです。
そして、先ほどの長くて難しい文章、{}
に含ませるidを決定する操作のことをgeneralizeといいます。汎用的になるんですね。すべての変数はgeneralizeされて{}
を持つことになります。
さて、ここまでで一応、多相型推論はできるようになったはずです。やってみましょう。
もう一度先ほどの画像です:
ここまででg
の型がわかっています。次はf
ですね。
画像内のコードの4行目でx [id=1]
の型が決定されます。
letでgが定義されているのでgの型には{}
が付き、gにxが適用されるのでg
がinstantiateされます。
{}の中身を表で計算してみましょう。
集合名 | 中身 |
---|---|
型環境に含まれるすべての型 | T(0), T(1), T(2), int, bool |
型環境に含まれるすべての型のid | 0, 1, 2 |
型環境に含まれる型の{} に含まれるもの |
|
型環境に含まれるすべての型のidから{} に含まれるものを除いたもの |
0, 1, 2 |
gの型に含まれるid | 2 |
gに含まれるidから, その型環境に含まれるすべての型のそれぞれに含まれるidからそのそれぞれの型の{}に含まれるidを取り除いたもの, を取り除いたもの | |
よって、{}の中身には何も入りません。 |
g
がinstantiateされましたが、{}
の中身が空なのでこの場合特に変化はありません。
f
の返却型はg x
よりbool
です。
このままだと、f
の型はT(2) -> T(2) -> bool
ですが、{}
のことを思い出してみましょう。
あのgeneralizeの長い難しい文章通りに手順を踏めば、{2}
という結果が得られるはずです。
やってみましょう。
以上が多相型推論の説明でした。この記事自体、急いで書いたので(ただいま 12/19 20:00)わかりづらいところや間違っている部分もあると思います。
コメントお待ちしております。