1.1 1930s Church 的 λ 演算:函數式程式的數學基石#
前言:在電腦誕生之前,先有了「計算」的定義#
1930年代,「電腦」這個詞還只指一種職業(做計算的人)。但數學基礎危機(Hilbert 的 Entscheidungsproblem,判定問題)逼使邏輯學家回答一個根本問題:「可計算」到底是什麼意思? 什麼樣的函數是「能有效算出來」的?
1936 年,Princeton 的 Alonzo Church 用一個極簡的符號系統——λ 演算(lambda calculus)——給出答案;同一年,他的學生 Alan Turing 用完全不同的圖像——圖靈機——給出另一個答案。驚人的是,兩者刻畫的「可計算函數」集合完全相同(Church-Turing 命題)。這場競爭(後來 Churchill 邀 Turing 當博士指導學生)確立了:計算不需要狀態機與記憶帶,只需要「函數套用」這一件事。
λ 演算的語法小到不可思議:
只有三種東西:變數、抽象(建立函數)、套用。整個函數式程式設計——從 Lisp 到 Haskell 到 JavaScript 的箭頭函數——都是這三個語法構造的工程化擴充。
三條歸約規則:λ 演算的全部語義#
λ 演算的「執行」就是規約(reduction),有三條規則:
| 規則 | 名稱 | 定義 |
|---|---|---|
| \(\alpha\) | 改名 | \(\lambda x. x \equiv_\alpha \lambda y. y\) (約束變數可改名) |
| \(\beta\) | 套用 | \((\lambda x. e_1)\; e_2 \to e_1[x := e_2]\) (代入) |
| \(\eta\) | 外延 | \(\lambda x.\, f\; x \to f\) ( \(x\) 不自由出現時) |
其中 \(\beta\) 規約是「函數呼叫」的數學定義。注意它沒有賦值、沒有狀態——這就是「純函數式」的哲學根源:程式的意義只是項到項的改寫。
一個完整例子:自然數(Church 數)#
Church 用「函數套用次數」編碼自然數:數字 \(n\) 是一個把函數套用 \(n\) 次的高階函數:
加法就是函數組合: \(\text{add} = \lambda m. \lambda n. \lambda f. \lambda x.\, m\; f\; (n\; f\; x)\) 。驗證 \(2 + 1\) :
add 2 1
= (λm.λn.λf.λx. m f (n f x)) (λf.λx. f (f x)) (λf.λx. f x)
→ (λn.λf.λx. (λf'.λx'. f' (f' x')) f (n f x)) (λf.λx. f x)
→ λf.λx. (λf'.λx'. f' (f' x')) f ((λf''.λx''. f'' x'') f x)
→ λf.λx. f (f (f x)) -- 即 Church 數 3
graph TD
A["add 2 1"] --> B["β-規約: 代入 m:=2"]
B --> C["λf.λx. 2 f (1 f x)"]
C --> D["β-規約: 展開 2 與 1"]
D --> E["λf.λx. f (f (f x))"]
E --> F["= Church 數 3 ✓"]布林值、序對與條件:一切皆函數#
連 if 都能用函數表達。定義真值為「選擇器」:
則 if c then a else b 就是 c a b——布林值本身「知道」如何選擇。序對(pair)同理: \(\text{pair} = \lambda a.\lambda b.\lambda f.\, f\; a\; b\) 。資料結構可以是函數,函數也可以是資料——這個等價性(函數是一等公民,first-class functions)要到 1958 年 Lisp 才真正被機器實現,但數學基礎在 1936 年就完成了。
歷史插曲:λ 演算差點死於「類型悖論」#
Church 原始的「無型別 λ 演算」有一個致命傷:自套用 \(\lambda x.\, x\; x\) 允許構造出不動點,進而(配合邏輯解釋)重新產生 Russell 悖論式的矛盾——1935 年 Kleene-Rosser 證明 Church 早期系統不一致。Church 的回應有二:一是將 λ 演算「降格」為純粹的計算系統(不管邏輯),二是後來發展簡單型別 λ 演算(1940)。前者成為 Lisp 的理論基礎(動態、無型別),後者成為 ML/Haskell 的祖先(靜態型別)。今天的函數式語言家族,正是這兩條路線的分岔與再會合。
λ 演算與求值順序:一個悬而未決的選擇#
\(\beta\) 規約可以在不同位置進行,產生不同的求值策略:
- 應用序(applicative-order):先算完參數再套用——今天的絕大多數語言(C、Python、ML、Scheme)。
- 常態序(normal-order):最左最外優先,需要時才展開參數——Church-Rosser 定理保證若項有常態形(normal form),常態序必能找到它。
Church-Rosser 定理(合流性,confluence)是 λ 演算最重要的定理:
這個定理在數學上說「規約順序不影響結果」,但在工程上留下伏筆:既然順序不重要,那能不能乾脆不算用不到的東西?——這就是 30 年後 Haskell 惰性求值(第3章)的理論根據。
程式碼:用 JavaScript 直接實作 Church 數#
λ 演算可以直接在今天的語言裡「運行」,說明它不是玩具而是真實的計算模型:
// Church 數:n = 把 f 套用 n 次
const zero = f => x => x;
const succ = n => f => x => f (n (f) (x));
const one = succ(zero);
const two = succ(one);
const three = succ(two);
// 加法 = 第二數把 f 接在第一數之後
const add = m => n => f => x => m (f) (n (f) (x));
// 轉回 JavaScript 數字以驗證
const toNum = c => c (x => x + 1) (0);
console.log(toNum(add(two)(three))); // 5
// 布林值與條件
const T = a => b => a;
const F = a => b => b;
const IF = c => t => e => c (t) (e);
console.log(IF (T) ("yes") ("no")); // "yes"
// 序對
const pair = a => b => sel => sel (a) (b);
const fst = p => p (T);
const snd = p => p (F);
console.log(toNum(fst(pair(two)(three)))); // 2
這段程式碼中沒有一個 if、沒有物件、沒有迴圈——只有函數。它就是 1936 年的「機器」。
歷史意義與後續發展#
λ 演算對後世的影響沿三條線展開:
- Lisp(1958):McCarthy 直接以 λ 演算為語義模型,Lisp 的
(lambda (x) e)就是 Church 的 \(\lambda x. e\) ;JavaScript(1995)則因借用 Scheme 的語義而成為「穿著 C 外衣的 λ 演算」,閉包、箭頭函數、Array.map全部源自本章。 - 型別理論:簡單型別 λ 演算 → Curry 型別指派 → Hindley-Milner 推導(第2章)→ 依值型別,構成 ML、Haskell、Coq/Lean 的血統。
- 編譯理論:λ 演算是函數式編譯器的中間表示(IR);continuation-passing style(第4章)本質上就是一種 λ 項改寫系統。
換句話說,今天每一門語言的「函數」概念,都是 1936 年那三條規則的直系後代。λ 演算與圖靈機的競爭還有一個深遠的教訓:兩個模型等價,但表達力相同不等於好用相同——λ 演算天生適合「組合與重用」,這個實務優勢最終化為整個函數式程式設計運動。
重點整理#
- λ 演算用「變數、抽象、套用」三個語法構造定義了可計算性,與圖靈機等價(Church-Turing 命題)。
- 語義只有 \(\beta\) 規約一條核心規則;沒有狀態、沒有副作用,「純函數式」由此定名。
- Church 數、布林值、序對證明「一切皆函數」,函數是一等公民的數學根據。
- Church-Rosser 合流性定理保證規約順序不影響結果,為惰性求值鋪路。
- 無型別路線通往 Lisp 與動態語言;簡單型別路線通往 ML 與 Haskell,兩條線在現代語言(Scala、Rust)中重新匯合。