1973 ML 型別推導誕生:Milner 與演算法 W 的魔法#
案發現場#
1970 年代初,愛丁堡大學的 Robin Milner 正在開發 LCF(Logic for Computable Functions)——一個交互式定理證明器。LCF 面臨一個兩難:使用者輸入的數學式子必須經過型別檢查,確保「證明步驟」不會產生偽證(例如把 \(1+1\) 誤當成 \(true+false\) )。但型別檢查要求使用者到處寫型別註記,這在交互式環境中令人窒息。
未解之謎是:能不能讓語言自動推導出所有運算式的型別,使用者一個型別都不用寫,卻仍然擁有完整的靜態型別安全?
當時學界的悲觀論調是:如果缺少型別註記,型別系統就無法判定(type inference 是不可判定的)。1969 年 J. Roger Hindley 在邏輯文獻中其實已證明:對於簡單型別 λ 演算的組合子版本,主型別(principal type)推導是可判定的。但沒有人把它用在真正的程式語言上。Milner 在 1973 年為 ML 實作了推導演算法,並在 1978 年的論文中形式化為演算法 W,加上參數多型(parametric polymorphism),成就了程式語言史上最優雅的型別系統。
偵查過程#
Milner 的核心靈感來自兩個來源:Hindley 的邏輯推導演算法,與 Curry-Howard 對應——型別就是命題,程式就是證明。他的推導分三步:
第一步:基本推導規則。 型別環境 \(\Gamma\) 下,推導是語法導向的:
應用規則中出現了「約束」: \(e_1\) 的參數型別 \(\tau'\) 必須與 \(e_2\) 的型別相等。演算法用替換(substitution,記為 \(\sigma = \{\alpha \mapsto int\}\) )來解這些約束。
第二步:逐步推導 id : α → α。 考慮 fun x -> x(ML 語法):
- 為 \(x\) 生成新鮮型別變數 \(\alpha_1\) ,環境為 \(\{x : \alpha_1\}\) 。
- 函數體 \(x\) 推導出型別 \(\alpha_1\) 。
- 函數整體型別為 \(\alpha_1 \to \alpha_1\) 。
- 函數定義結束時,對 \(\alpha_1\) 進行泛化(generalization): \(\alpha_1\) 是區域變數、未被外部約束,故升格為全稱量詞變數,得到:
這就是 id 函數的主型別——它對所有型別一體適用。當呼叫 id 5 時,實例化為 \(\alpha \mapsto int\) ,得到 \(int \to int\) 。
第三步:演算法 W。 完整演算法由三個操作構成:
- Var:查環境,泛化後回傳。
- App:推導函數與參數,合一 \(fn : \alpha \to \beta\) 與 \(arg : \tau\) ,回傳 \(\beta\) 。
- Abs:為參數生成新鮮變數,推導函數體,回傳箭頭型別。
演算法 W 的性質是完備且健全(sound and complete):若存在主型別,W 必能找到;W 找到的型別必是最一般的。
第四步:代數資料型別。 ML 還引入了可定義的遞迴資料型別:
datatype tree = Leaf | Node of tree * int * tree
型別推導與代數資料型別結合,使 ML 能在「零註記」下處理任意複雜的資料結構。這個組合直接成為 OCaml、Standard ML、F#、Haskell 的核心。
結案報告#
ML 型別推導的遺產:
- 語言系譜:Standard ML(1983)、OCaml、F# 直承 ML 血統;Haskell(1990-Haskell.md)採用 Hindley-Milner 為基礎;Rust 的型別推導與所有權系統、Swift 的型別推導,都是這條血脈的後裔。
- 理論影響:Curry-Howard 對應從此成為程式語言理論的核心,型別系統研究(子型別、型別類、依值型別)皆以 ML 為起點。
- 工業實踐:現代語言(TypeScript、Scala、Kotlin、Go 的部分推導)都提供某種程度的型別推導。「靜態型別但不用寫型別」已成為主流體驗。
- 工具鏈:IDE 的型別顯示、重構工具、形式化驗證(Coq、Agda)都建立在 Milner 的基礎上。
Milner 於 1991 年獲得圖靈獎。ML 證明:型別安全與程式簡潔不必二選一——只要推導演算法夠聰明。
證據與工具#
OCaml 示範——零註記的型別推導:
let id x = x (* id : 'a -> 'a *)
let compose f g x = f (g x) (* compose : ('a -> 'b) -> ('c -> 'a) -> 'c -> 'b *)
type tree = Leaf | Node of tree * int * tree
let rec sum = function
| Leaf -> 0
| Node (l, v, r) -> sum l + v + sum r (* sum : tree -> int *)
let _ = id 5 (* int *)
let _ = id "hello" (* string *)
用 Python 模擬 Hindley-Milner 演算法 W:
import itertools
class TyVar:
_ids = itertools.count()
def __init__(self): self.id = next(TyVar._ids)
def __repr__(self): return f"t{self.id}"
class TyArrow:
def __init__(self, a, b): self.a, self.b = a, b
def __repr__(self): return f"({self.a} -> {self.b})"
def occurs(v, t):
if isinstance(t, TyVar): return v is t
if isinstance(t, TyArrow): return occurs(v, t.a) or occurs(v, t.b)
return False
def subst(t, s):
if isinstance(t, TyVar): return s.get(t, t)
if isinstance(t, TyArrow):
return TyArrow(subst(t.a, s), subst(t.b, s))
return t
def unify(a, b, s):
a, b = subst(a, s), subst(b, s)
if isinstance(a, TyVar) and not occurs(a, b): s[a] = b
elif isinstance(b, TyVar) and not occurs(b, a): s[b] = a
elif isinstance(a, TyArrow) and isinstance(b, TyArrow):
unify(a.a, b.a, s); unify(a.b, b.b, s)
elif a != b: raise TypeError(f"cannot unify {a} with {b}")
def infer(env, e, s):
if isinstance(e, str): # 變數
return subst(env[e], s)
if isinstance(e, tuple) and e[0] == "abs": # fun x -> body
v = TyVar(); env = dict(env); env[e[1]] = v
body = infer(env, e[2], s)
return TyArrow(subst(v, s), body)
if isinstance(e, tuple) and e[0] == "app": # f x
tf = infer(env, e[1], s); tx = infer(env, e[2], s)
res = TyVar()
unify(tf, TyArrow(tx, res), s)
return res
# 推導 id = fun x -> x
s = {}
t = infer({}, ("abs", "x", "x"), s)
print(t) # (t0 -> t0),即 ∀α. α -> α