06.3 依賴型別:當型別可以依賴於值的時候#
引言#
一路走來,我們不斷逼近同一個極限:「型別」只能談型別,不能談值。Vector<double> 可以說「這是向量」,說不出「這是長度 3 的向量」。「長度」是值,型別系統夠不著——於是長度錯誤淪為執行期異常(IndexError)。
依賴型別(dependent type)打破這道牆:允許型別以值為參數。Vec(Double, 3) 是一個型別,Vec(Double, 4) 是另一個。當型別能談值,型別檢查就升格為數學證明;當型別即命題,程式即證明——Curry–Howard 從 04 章的隱喻變成日常工作工具。Lean、Coq、Agda 的形式化數學大廈,全部蓋在這一個觀念上。
1. 定義:依賴函數型別( \(\Pi\) 型)與依賴積型( \(\Sigma\) 型)#
定義 1.1(依賴函數型 / \(\Pi\) 型). 對型別 \(A\) 與(依 \(a : A\) 而定的)型別族 \(B(a)\) :
其元素是函數 \(f\) ,滿足對每個 \(a : A\) , \(f(a) : B(a)\) 。回傳型別依輸入值而定。
定義 1.2(依賴積型 / \(\Sigma\) 型).
其元素是配對 \((a, b)\) ,其中 \(b : B(a)\) 。第二欄位的型別依第一欄位的值而定。
特例即普通型別:若 \(B\) 不依賴 \(a\) ,則 \(\Pi_{a:A} B = A \to B\) 、 \(\Sigma_{a:A} B = A \times B\) 。依賴型別是 05 章積和冪的推廣,不是另一套系統。
| 非依賴 | 依賴 | 數學原型 |
|---|---|---|
| \(A \to B\) | \(\Pi_{a:A} B(a)\) | 笛卡兒積族、纖維化 |
| \(A \times B\) | \(\Sigma_{a:A} B(a)\) | 積分: \(\int_A B(a)\, da\) 、纖維叢 |
\(\Sigma\) 的基數是 \(\sum_{a} |B(a)|\) 、 \(\Pi\) 的基數是 \(\prod_a |B(a)|\) ——記號就是定義。05 章的「型別即代數式」在此升級為「型別即無限代數式(級數/積分)」。
2. 定理:依賴型別即命題——從標籤到數學#
定理 2.1(Curry–Howard 的完整形態). 在依賴型別論中:
- 命題 = 型別(包括帶參數的命題,如「 \(m + n = k\) 」);
- 證明 = 程式;
- 型別檢查 = 證明檢查(機械化、可判定);
- 一致性 = 沒有型別為
Empty的閉項(04.2 定理 3.3 的依賴版)。
於是可以把數學定理寫成型別,把證明寫成程式:
-- Lean 4:加法交換律,證明就是一個「型別為定理」的項
theorem add_comm (n m : ℕ) : n + m = m + n := by
induction n with
| zero => simp
| succ k ih => simp [Nat.succ_add, ih]
-- 這段「程式」通過型別檢查 = 定理被形式化證明
-- 註:Lean 中 `n + m = m + n` 本身就是一個型別(Eq 是歸納型別)!
-- 「長度 n 的向量」:依賴型別的招牌例子
inductive Vec (α : Type) : Nat → Type where
| nil : Vec α 0
| cons : α → {n : Nat} → Vec α n → Vec α (n + 1)
-- 「安全取第 i 項」:i < n 這個**數學條件**直接寫進型別
def safeGet {α : Type} {n : Nat} (v : Vec α n) (i : Nat) (h : i < n) : α :=
match v, i, h with
| Vec.cons x _, 0, _ => x
| Vec.cons _ xs, i + 1, _ => safeGet xs i (Nat.lt_of_succ_lt_succ h)
-- 呼叫 safeGet v 5 h 時,h : 5 < n 必須在編譯期存在:
-- 越界不再是執行期異常,而是型別錯誤!
# 對照:Python 只能「執行期檢查 + 型別註記閹割版」
from typing import Generic, TypeVar, NewType
T = TypeVar("T")
Length = NewType("Length", int)
class Vec(Generic[T]):
def __init__(self, items: list[T]):
self.items = items
def __len__(self) -> int:
return len(self.items)
def safe_get(v: Vec[T], i: int) -> T:
# 長度條件無法進入型別;只能執行期檢查
if i >= len(v.items):
raise IndexError(f"index {i} out of range for length {len(v.items)}")
return v.items[i]
# 差距一目了然:依賴型別把「if 檢查」搬進了型別系統
3. 範例:依賴型別的三個層次#
依賴型別的威力按「編碼深度」遞增:
3.1 層次一:把資料不變式編入型別#
-- 矩陣乘法:只有「內維吻合」的呼叫能通過編譯
def matMul {m n p : Nat} (A : Mat m n) (B : Mat n p) : Mat m p := ...
-- matMul A B 若 A 的行數 ≠ B 的列數,根本型別不合
-- 對照 NumPy:shape mismatch 是執行期 ValueError
3.2 層次二:把規格編入型別(refinement)#
-- 「質數」作為型別:{ p : Nat // IsPrime p }(子型別/refinement)
def IsPrime : Nat → Prop
| 0 | 1 => False
| p => p ≥ 2 ∧ ∀ d, d ∣ p → d = 1 ∨ d = p
def Prime : Type := { p : Nat // IsPrime p }
-- 函數「給定任意 n > 1,存在質數 p > n」(歐幾里得)
theorem exists_prime_gt : ∀ n : Nat, ∃ p, p > n ∧ IsPrime p := ...
-- ∃ 是 Σ 型的 Prop 版;∀ 是 Π 型——邏輯連接詞全部成為型別構造子
3.3 層次三:宇宙層級——依賴型別論自身需要型別#
-- 當型別可以依賴值,「所有型別的型別」= 宇宙(universe)
-- 但 Type : Type 會重建羅素悖論(Girard 悖論,1972)!
-- 解法:層級宇宙——這正是 04.3 羅素分層的再現
#check (Type 0 : Type 1) -- Type 0 是 Type 1 的元素,但不包含自身
Girard 悖論(Type : Type 的矛盾)告訴我們:依賴型別論沒有逃出 04.3 的陰影,只是每一層都要重新蓋防火牆。強正規化(04.2)也以更複雜的形式被重新證明(Girard 的 \(\Lambda\) 範疇法),以保證證明檢查終止。
4. 範例:證明助理的一天——二分查找的正確性#
依賴型別最動人的應用:程式與其正確性證明一起交付。
-- 已排序陣列的二分查找:回傳「要嘛找到(帶索引),要嘛證明不存在」
inductive FindResult (a : Type) (xs : Array a) (target : a) : Type where
| found : (i : Nat) → (i < xs.size) → xs[i] = target → FindResult a xs target
| notFound : (∀ i, (h : i < xs.size) → xs[i] ≠ target) → FindResult a xs target
-- 這個和型的兩個分支各自攜帶「證明」:
-- found 帶「這個索引真的是目標」的證明
-- notFound 帶「任何索引都不是」的證明——不可能出現「沒找到但其實在」的 bug
Python 中同等規格只能靠註解與測試(契約,不是證明):
def binary_search(xs: list[int], target: int) -> int | None:
"""回傳 target 的索引,不存在則 None。
註解說「不存在則 None」——但編譯器不知道,只有測試知道。
依賴型別版本(上方 Lean)把這句話變成型別的一部分。"""
lo, hi = 0, len(xs)
while lo < hi:
mid = (lo + hi) // 2
if xs[mid] == target: return mid
if xs[mid] < target: lo = mid + 1
else: hi = mid
return None
這就是 形式驗證(formal verification) 的實務樣貌:seL4 作業系統核心、CompCert C 編譯器、Fiat Cryptography(TLS 憑證庫)都在 Coq/Lean 系譜上以依賴型別完成了機器檢查的正確性證明。
5. 定理:代價與張力——圖靈完備 vs 全面終止#
定理 5.1(停機問題的陰影). 若型別語言的項級計算是圖靈完備的,則「型別檢查」(需要對依賴型別做求值比較)不可判定。
工程解法:證明助理要求所有函數全面終止(structural recursion 或 well-founded recursion 檢查),使型別檢查可判定、邏輯一致;代價是寫不出一般遞迴——「想寫不終止的程式?請離開證明層,用 IO monad/不檢查的逃生口」。這是 04.2 結尾那條張力的最終形態:
flowchart TD
A["STLC (1940)<br/>強正規化"] --> B["System F (1971)<br/>+∀, 仍強正規化"]
B --> C["依賴型別論 (1972-)<br/>+值依賴, 需宇宙層級防火牆"]
C --> D{圖靈完備?}
D -->|是| E["邏輯不一致<br/>Girard 悖論"]
D -->|否: 全套檢查| F["證明助理: Lean/Coq/Agda<br/>一致 + 可判定檢查"]
F --> G[形式化數學<br/>液體張量猜想, FLT 計畫]6. 互相驅動:三股力量的匯流#
- 數學驅動:四色定理(1976,電腦輔助)引發「證明可信度」爭論;Gonthier 用 Coq 完整形式化(2005)平息之。依賴型別從此是數學的基礎設施。
- 工程驅動:航空、密碼學、區塊鏈對「零 bug」的需求,讓形式驗證從學術走進產業(Rust 的型別系統也是這條光譜上的溫和版本)。
- 兩者回饋:Lean 4 本身用 Lean 寫成(自舉),編譯器即定理——理論與工程在同一個依賴型別宇宙中閉環。
回望全程:羅素悖論(04.3)→ 型別防火牆 → STLC 強正規化(04.2)→ 型別即安全網(04.1)→ 積和冪的代數(05)→ \(\forall\) 與 \(<:\) 的一般化(06.1–06.2)→ 值依賴的 \(\Pi\) / \(\Sigma\) (本節)。型別論從救火隊成長為數學與程式共同的家。
7. 章末小結#
- 依賴型別允許型別依賴值: \(\Pi\) 型(回傳型別依參數)、 \(\Sigma\) 型(欄位型別依前欄值);非依賴型別是其特例。
- 基數記號 \(\prod/\sum\) 與型別構造子一一對應,05 章的代數視角升級為級數/積分。
- Curry–Howard 完整化:命題即型別、證明即程式、型別檢查即證明檢查。
Vec α n、refinement 型別、FindResult:資料不變式與規格編入型別。Type : Type重建悖論(Girard),宇宙層級是 04.3 分層的再現;全套檢查保住一致性,圖靈完備性被置於證明層之外。
下一步#
你已握有型別論的完整光譜:從錯誤檢查到定理證明。接下來的章節將沿兩個方向繼續:(1) 範疇論視角——把積、和、冪、 \(\Pi\) 、 \(\Sigma\) 統一為極限與餘極限,型別論成為一個拓撲/幾何物件(homotopy type theory 的門檻);(2) 計算效應——當函數要與世界互動(IO、狀態、異常),純函數型別如何擴充(monad 即內部語言的擴張)。兩條路都從你此刻站立的這塊基石出發。