1983:Standard ML 誕生#
事件#
1983 年起,愛丁堡大學的 Robin Milner 與 Mads Tofte 開始將 LCF 定理證明器所用的詮釋語言 Edinburgh ML 系統化為一門正式定義的語言——Standard ML(SML)。參與者還包括 Robert Harper、David MacQueen 等人,最終於 1990 年出版《The Definition of Standard ML》(1997 年修訂版),成為少數擁有「形式化語意定義」的程式語言。Standard ML 是第一門將 Hindley–Milner 型別系統完整化、並配備模組系統的靜態型別函數式語言。
語法/特性加入的理論與實用原因#
1. Hindley–Milner 型別推斷的完整化#
- 理論原因:Milner 在 1978 年發表的論文證明了 HM 型別系統的型別推斷演算法(Algorithm W)具有完整型別推斷能力——程式設計師幾乎不用寫型別宣告,編譯器就能自動推導出所有型別,且型別錯誤在編譯期即被捕捉。1983 年的 Standard ML 將 Damas–Milner 演算法正式納入語言定義,並處理了多態參照(polymorphic references)等邊界問題(value restriction)。
- 實用原因:LISP 的動態型別使得大型系統的錯誤往往延遲到執行期才爆發;SML 讓大型系統在編譯期就獲得型別安全保證,同時保留「不用寫型別」的簡潔性。
- 彌補缺陷:Edinburgh ML 雖有型別推斷但語言本身非正式、實作間行為不一致。
fun map f [] = []
| map f (x::xs) = f x :: map f xs
(* 型別:('a -> 'b) -> 'a list -> 'b,完全自動推斷 *)
呼叫端完全不必標註型別,編譯期即檢查錯誤——對照 LISP 同樣的程式要到執行期才發現型別不符:
val squares = map (fn x => x * x) [1, 2, 3] (* 推斷為 int list:[1, 4, 9] *)
(* map (fn x => x + 0.5) [1, 2, 3] (* 編譯錯誤:int 與 real 不相符 *)
2. 模組系統:structure、signature、functor#
- 理論原因:Milner 將「函數式抽象」的思想提升到模組層級——functor 就是「以模組為參數、以模組為返回值」的函數,signature 則相當於模組的型別。這是參數化多型在模組系統的推廣,成為後世 ML 系模組語言(OCaml、Agda 模組系統)的藍本。
- 實用原因:LCF 的策略(tactics)函式庫需要良好的封裝與複用機制;大型定理證明與編譯器專案需要明確的介面分離。
- 彌補缺陷:LISP 沒有真正的模組系統,Edinburgh ML 只靠命名慣例分隔。
signature ORDERED = sig
type t
val compare : t * t -> order
end
functor MakeSet (O : ORDERED) = struct
datatype set = Empty | Node of set * O.t * set
fun insert (Empty, x) = Node (Empty, x, Empty)
| insert (Node (l, y, r), x) =
case O.compare (x, y) of
LESS => Node (insert (l, x), y, r)
| EQUAL => Node (l, y, r)
| GREATER => Node (l, y, insert (r, x))
end
(* 同一個 functor 可套用到任何有序型別 *)
structure IntSet = MakeSet (struct type t = int fun compare (a,b) = Int.compare (a,b) end)
structure StringSet = MakeSet (struct type t = string fun compare (a,b) = String.compare (a,b) end)
這正是「SML 需靠 functor 才能泛化數值程式」的具體寫法——抽象與實作在模組層級分離,對照 LISP 只能靠命名慣例(如 mylib-insert)模擬封裝。
3. datatype 宣告與模式匹配#
- 理論原因:源自 1970 年代對代數資料型別(algebraic data types)的研究,
datatype允許定義和型別(sum type),搭配模式匹配做結構化解構,對應 HOPE 語言的實驗成果。 - 實用原因:定理證明器需要表示語法樹與證明項,和型別是天然的工具。
- 彌補缺陷:LISP 需用 cons cell 與符號手動編碼資料結構,缺乏編譯期檢查。
datatype expr = Const of int
| Var of string
| Plus of expr * expr
| Times of expr * expr
(* 模式匹配做結構化解構:求值語法樹 *)
fun eval env (Const n) = n
| eval env (Var name) = env name
| eval env (Plus (a, b)) = eval env a + eval env b
| eval env (Times (a, b)) = eval env a * eval env b
val tree = Plus (Const 3, Times (Const 2, Var "x"))
val result = eval (fn "x" => 5 | _ => 0) tree (* [13] *)
若新增建構子(如 Minus)而漏掉某個匹配分支,編譯器會發出「non-exhaustive match」警告——LISP 的 cons cell 編碼完全得不到這種檢查。
4. 例外處理(exception)#
- 理論原因:在靜態型別語言中引入非局部跳脫需要與型別系統整合;SML 的 exception 是一等值的「和型別的動態成員」,由 raise 與 handler 配對。
- 實用原因:LCF 中戰術失敗需要即時中止回溯,例外處理是控制搜尋流程的自然機制。
- 彌補缺陷:Edinburgh ML 只有簡陋的錯誤中止;LISP 的錯誤機制依賴動態作用域難以分析。
exception 與 raise/handle 配對的典型用法——戰術失敗時回退到另一個策略:
exception Failure of string
fun tactic1 goal = raise Failure "tactic1 失敗"
fun tactic2 goal = "證明完成"
(* handle 捕捉例外並提供備案,控制搜尋流程 *)
fun tryTactics goal =
(tactic1 goal; tactic2 goal)
handle Failure msg => (print ("回退:" ^ msg); tactic2 goal)
彌補了什麼缺陷#
Standard ML 彌補了兩大缺陷:其一是 LISP 系語言的動態型別無法支撐大型系統——SML 用完整化的 Hindley–Milner 型別推斷達成「動態語言的便利 + 靜態語言的安全」;其二是 Edinburgh ML 缺乏正式標準——《The Definition of Standard ML》使其成為語意形式化的典範。它的模組系統與 datatype 更直接影響了 1990-CAML誕生 與 1996-OCaml誕生。