1970:ML 誕生於 LCF 定理證明器#
事件#
1970年,Robin Milner 在愛丁堡大學為 LCF(Logic for Computable Functions,可計算函數的邏輯)定理證明器設計了 ML(Meta Language,中介語言)。ML 原本不是獨立語言,而是讓使用者撰寫證明策略、操作定理的「指令語言」。Milner 隨後發展出 Hindley–Milner 型別系統與 W 演算法(1975 年正式發表),ML 逐漸成為獨立的通用函數式語言,並衍生出 Standard ML(1983 起)與 Caml(1985)/OCaml(1996)。
語法/特性加入的理論與實用原因#
1. 型別推斷(Hindley–Milner 型別系統)#
- 理論原因:定理證明必須保證證明程式本身無型別錯誤——「型別正確的程式不會出某些類的錯」正是數學嚴謹性的要求。Milner 借鑑 Hindley(1969)的型別演算法,設計出可推斷型別的系統,並以 W 演算法(1975 年發表)實現。
- 實用原因:要求使用者手寫型別宣告會拖慢互動式定理證明的節奏;型別推斷讓編譯器自動算出所有型別。
- 彌補缺陷:LISP 的動態型別讓許多錯誤(把符號當數字加)要執行到才會發現;ML 在編譯期就攔下這類錯誤。
fun fact 0 = 1
| fact n = n * fact (n - 1)
(* ML 自動推斷 fact : int -> int *)
完全不必寫型別宣告,編譯器(或 REPL)自動推得每個值的型別:
- fun double n = n * 2;
val double = fn : int -> int ; 由 * 推得 n 是 int
- fun add a b = a + b;
val add = fn : int -> int -> int ; 由 + 推得兩個引數都是 int
- val msg = "hello";
val msg = "hello" : string ; 字面值直接定案
- fun f x = if x > 0 then x else 0;
val f = fn : int -> int ; if 的兩支型別必須一致,推得 int
對照組:LISP 動態型別要到執行期才爆發錯誤:
;; LISP:(+ 1 "a") 直譯器不會事先擋下,執行到才報錯
(+ 1 "a")
; 執行期錯誤:"a" 不是數字
;; 若這行程式藏在幾萬行之外、罕用的分支裡,
;; 可能上線數月後才第一次被執行到
2. 靜態強型別但不用寫型別宣告#
- 理論原因:Hindley–Milner 型別系統是「參數多型」的單型化(let-polymorphism),兼顧嚴謹與簡潔。
- 實用原因:達成「靜態型別的安全性 + 動態型別的書寫便利」兩者兼得。
- 彌補缺陷:同時彌補 LISP(無靜態型別保障)與 ALGOL/FORTRAN(必須手寫型別)兩方的缺陷。
對照組:ALGOL 60 / FORTRAN 必須手寫型別宣告;ML 的推斷省去這層負擔,卻保留同樣的靜態保障:
C FORTRAN:每個變數都要宣告型別
INTEGER N
REAL X
N = 3
X = 1.5
(* ML:不寫任何型別宣告,錯誤照樣在編譯期被攔下 *)
val n = 3
val x = 1.5
(* n * x → 編譯期型別錯誤:int 與 real 不能直接相乘 *)
(* 而在 LISP 中 (+ 3 1.5) 會安靜地算出 4.5,隱藏的精度問題到執行期才浮現 *)
3. 參數多型(Parametric Polymorphism)#
- 理論原因:
length : 'a list -> int這種「對任意型別都成立」的函數在數學上就是自然的泛函,型別系統應能表達之。 - 實用原因:定理證明器處理各種資料結構,容器操作需要通用化,否則要為每種型別複製一份函數。
- 彌補缺陷:當時主流靜態語言沒有泛型;不寫泛型的代價是大量重複程式碼。
length 的型別 'a list -> int 是推斷出來的參數多型——同一份程式碼對任何元素型別都成立:
- fun length nil = 0
= | length (_ :: t) = 1 + length t;
val length = fn : 'a list -> int ; 『a 是型別變數:任意型別
- length [1, 2, 3];
val it = 3 : int
- length ["a", "b"];
val it = 2 : int ; 同一份程式碼,字串列表也適用
- length [(1, "x"), (2, "y")];
val it = 2 : int ; 配對列表也適用
對照組:沒有泛型的靜態語言得為每種型別複製一份函數:
;; 無泛型時的慘況(C 語言風格的複製貼上):
int length_int(int a[], int n) { /* ... */ }
int length_str(char *s[], int n) { /* 幾乎相同的程式碼 */ }
int length_pair(...) { /* 又一份相同的程式碼 */ }
4. 模式匹配(Pattern Matching)#
- 理論原因:數學定義本來就是「按結構分情形定義」——0 的階乘是 1、n 的階乘是 n×(n−1)!。模式匹配把這種定義方式直接搬進語言。
- 實用原因:定理證明需要解構代數資料型別(項、公式、證明樹),模式匹配是最自然的寫法,且編譯器能檢查所有情形是否涵蓋(exhaustiveness check)。
- 彌補缺陷:LISP 的 cond 需手動呼叫謂詞逐一測試,容易漏掉情形;模式匹配把遺漏變成編譯錯誤。
模式匹配按資料結構分情形定義,編譯器還會檢查涵蓋性:
(* 按結構分情形:與數學定義逐字對應 *)
fun fact 0 = 1
| fact n = n * fact (n - 1);
(* 模式匹配解構列表:空列表與「頭 :: 尾」兩種情形 *)
fun sum nil = 0
| sum (h :: t) = h + sum t;
(* 模式匹配解構配對 *)
fun fst (x, _) = x;
fun snd (_, y) = y;
fst (3, "x"); (* => 3 *)
對照組:LISP 用 cond + 謂詞手動分支,漏掉一種情形編譯器不會警告:
;; LISP cond:若忘了 (null l) 這一條,
;; (car nil) 會在執行期才爆炸,且編譯器無從警告
(defun sum (l)
(cond ((atom l) l) ; 只測了 atom,忘了處理空列表的語義
(t (+ (car l) (sum (cdr l))))))
(* ML:若只寫一條子句,編譯器立即發出警告 *)
- fun sum (h :: t) = h + sum t;
(* 警告:match nonexhaustive;nil 的情形沒有涵蓋 *)
彌補了什麼缺陷#
ML 證明了「靜態強型別」與「函數式便利」可以並存:型別推斷免除了型別宣告的負擔,參數多型免除了程式碼重複,模式匹配免除了手動分支檢查。它彌補的最大缺陷是 LISP 動態型別「錯誤要到執行期才爆發」的問題——對必須絕對嚴謹的定理證明而言,這是生死攸關的差別。Hindley–Milner 型別系統此後成為 Haskell、F#、Rust 等語言的基石。