02.2 β化約與無值型蘭布達演算的表達力#
導言:一條代換規則能走多遠?#
上一章我們建立了 λ 演算的文法與 β 化約。本章回答一個挑戰性的問題:只有「把參數代進函數體」這一個動作,如何產生條件分支、資料結構、乃至遞迴? 答案將展示:β 化約不只是計算的機制,它就是計算的全部——而它的表達力恰好等同圖靈機。
「無值型」(untyped,亦譯「非型別化」)λ 演算是邱奇的原始版本:任何項都可以應用於任何項。這份自由既孕育了驚人的表達力(本章),也埋下了危險(某些項沒有正規形式——即死迴圈),並最終引出了第三章的型別論。讓我們先享受自由。
布林值:if-then-else 是一個函數#
在 λ 演算中,資料就是它自己的使用說明書。定義:
這叫教會布林(Church booleans)。它們的聰明之處:TRUE 是「選第一個」的函數,FALSE 是「選第二個」的函數。因此 if-then-else 不需要語法,就是應用:
驗證:
未被選中的分支仍然會被「寫進」代換裡,但在正規順序(normal order,最左最外優先)求值下不會被執行。這正是惰性求值的理論根據:if cond then crash else 42 在 cond 為真時不會 crash。
-- Haskell:這些定義不只是類比,教會布林就是這樣用的理論基礎
let tru = \x -> \y -> x
let fls = \x -> \y -> y
let iff b t f = b t f
-- 布林運算也全是函數組合:
let and' = \p -> \q -> p q fls -- p 真→回 q;p 假→回 fls
let or' = \p -> \q -> p tru q
let not' = \p -> p fls tru
main = print (iff (and' tru fls) "yes" "no") -- "no"
自然數:教會數字與疊代#
定義(教會數字): \(n\) 表示為「把函數 \(f\) 應用 \(n\) 次」:
零是「什麼都不做」,三是「疊代三次」。這個定義的深意:自然數 = 疊代的次數。它與皮亞諾公理「零 + 後繼」的結構完美同構,也與函數式程式的 fold/foldr 概念同源。
加法與乘法因而是「疊代的疊代」:
驗證 \(\mathrm{ADD}\ \bar{2}\ \bar{3}\) :先給 \(n=\bar 3\) 把 \(f\) 疊 3 次,再讓 \(m=\bar 2\) 把「疊 3 次」再疊 2 次——總共疊 5 次。 \(\checkmark\)
# Python 中親手操作教會數字
SUCC = lambda n: lambda f: lambda x: f(n(f)(x)) # 後繼
ADD = lambda m: lambda n: lambda f: lambda x: m(f)(n(f)(x))
MUL = lambda m: lambda n: lambda f: m(n(f))
def church(n): # int -> 教會數字
return lambda f: lambda x: f(f(f(...))) if False else (
(lambda f, x, n: (lambda k: (lambda g: g)(lambda _:
f(f(f(x)))) ) ) ) # 遞迴版本見下,此處直接展開
def to_int(cn): # 教會數字 -> int
return cn(lambda i: i + 1)(0)
zero = lambda f: lambda x: x
one = SUCC(zero)
two = SUCC(one)
three = SUCC(two)
five = ADD(two)(three)
six = MUL(two)(three)
print(to_int(five), to_int(six)) # 5 6
條件判斷「 \(n\) 是否為零」:
把常數函數疊 \(n\) 次:疊零次得 TRUE,疊一次以上得 FALSE。零的偵測是「疊代器的天性」。
不動點:遞迴如何從虛空中出現#
現在來到本章的高潮。想定義遞迴函數(如階乘),需要函數能呼叫「自己」。但 λ 項是匿名的——函數體內沒有名字可用來指稱自己!邱奇如何突破?
第一步:把「自己」當參數傳進來#
寫階乘時,不直接呼叫自己,而是接受一個「假想的自己」作為參數:
這是開放的遞迴: \(F\) 還需要一個 self。但我們有個自欺欺人的妙招——把 \(F\) 自己傳給自己: \(F\ F\) 。因為 self 的位置正好收到 \(F\) ,函數體內的 self self 就化約為 \(F\ F\) ——遞迴成立了!
\(F\ F\) 在化約中不斷重新生成自己——這就是不動點的動態面目。
第二步:抽象出通式——Y 組合子#
「 \(F\ F\) 產生 \(F\ F\) 」的現象可以一般化。觀察:若存在項 \(Y\) 使得
則 \(Y\ F\) 就是 \(F\) 的不動點(fixed point)—— \(F\) 應用上去不變。邱奇的學生 Curry 找到了(邱奇版本稍異,通稱 Y 組合子):
驗證(一步 β 化約即見證奇蹟):
\(Y\ F\) 化約一步後,以 \(F\ (Y\ F)\) 的形態重現——每化約一次,就把自己展開一層。遞迴 = 不斷重生的不動點。
# Y 組合子在 Python 中的活體展示
Y = (lambda f: (lambda x: f(lambda v: (x(x))(v)))
(lambda x: f(lambda v: (x(x))(v))))
# 階乘:完全匿名、完全無遞迴關鍵字
factorial_gen = lambda self: lambda n: 1 if n == 0 else n * self(n - 1)
factorial = Y(factorial_gen)
print(factorial(10)) # 3628800 —— 來自一個沒有 def、沒有名字的世界
-- Haskell 版(注意:Haskell 需要小心 bottom,此處用惰性使 Y 可用)
newtype Mu a = InF { outF :: Mu a -> a }
y :: (a -> a) -> a
y f = g (InF g) where g x = f (outF x x)
-- Mu 型別是「自指型別」:Mu a 裝著一個「吃 Mu a 給 a」的函數
-- 這正是 (x x) 需要的:x 既是函數又是參數
-- 無型別 λ 演算允許 x x;型別化之後,x x 直接型別錯誤!
-- 這就是「無值型的表達力 vs 型別化的一致性」張力的具體落點。
Ω:發散的原型#
Y 的原料是 \((\lambda x.\,x\ x)\,(\lambda x.\,x\ x)\) ——著名的 Ω:
Ω 化約一步回到自己:永不完結。它是停機問題在 λ 演算裡的具體化身(回憶 01.2:無窮迴圈程式)。而 \(Y\ F\) 可以看成「馴化的 Ω」:Ω 直接自食, \(Y\ F\) 則在自食之間「經手」 \(F\) ,每次重生都推進一點計算。無窮與生產力只隔一個函數應用。
兩種求值策略:正規順序與應用順序#
β 化約可以多個「紅色子項」(形如 \((\lambda x.\,M)\ N\) 的位置)同時存在,先化約哪個?兩大策略:
| 策略 | 規則 | 對應語言 |
|---|---|---|
| 正規順序(normal order) | 最左最外優先;參數先不代換 | Haskell(惰性)、Call-by-need |
| 應用順序(applicative order) | 先化約參數再應用 | Python、Scheme(急切)、Call-by-value |
關鍵定理(標準化定理,standardization theorem 的推論):
定理:若項 \(M\) 有正規形式(normal form,不再可化約),則正規順序求值必然找到它;應用順序則可能在半路撞上無窮迴圈。
實例: \((\lambda x.\,\bar{1})\ \Omega\) 。
- 正規順序:直接得 \(\bar{1}\) ,完成。✓
- 應用順序:先化約 Ω——永無寧日。✗
這解釋了 Haskell 的設計哲學(惰性求值 = 正規順序的工程化),也解釋了為什麼 Python 中 f(g(x)) 一定先算 g(x):應用順序是它的理論基因。但也注意:急切求值效率高(不重複求值),惰性求值用共享(sharing / call-by-need)補救——理論的兩難延伸成語言的譜系。
flowchart TD
M["λ 項"] -->|"β 化約一步"| M2["λ 項"]
M2 -->|"..."| NF["正規形式<br>(答案)"]
M2 -.->|"可能永不終止"| OMEGA["Ω / 發散"]
M -->|"正規順序"| NF2["保證到達正規形式(若存在)"]
M -->|"應用順序"| NF3["可能卡死在無用的子計算"]
style NF fill:#c8e6c9
style OMEGA fill:#ffcdd2表達力封頂:與圖靈機等價#
本章的構件——教會布林、教會數字、配對( \(\mathrm{PAIR} = \lambda a.\,\lambda b.\,\lambda f.\,f\ a\ b\) )、Y 組合子——合起來足以在 λ 演算內模擬任意图靈機:用教會數字編碼紙帶內容、用函數組合表示狀態轉移、用 Y 驅動每一步。因此:
定理:λ-可計算函數 = 圖靈可計算函數 = 部分遞迴函數。
同時,λ 演算也有自己的「停機問題」:正規形式的存在性不可判定(給定 \(M\) ,判斷它是否會化約到正規形式,等價於停機問題)。第二章結尾(02.3)我們會把這個等價性具體演給你看。
範例綜合:用純 λ 演算寫「前驅」#
乘法、加法都好辦,前驅( \(n-1\) )才是教會數字的試金石——因為疊代器只會前進,如何後退?邱奇的解法是攜帶「舊值」的疊代:
# 前驅函數的化約過程(Python 可執行版)
PRED = (lambda n: lambda f: lambda x:
n(lambda g: lambda h: h(g(f)))(lambda u: x)(lambda u: u))
to_int = lambda c: c(lambda i: i + 1)(0)
print(to_int(PRED(three := (lambda f: lambda x: f(f(f(x))))))) # 2
原理:疊代 \(n\) 次的過程中,攜帶一個「慢一拍」的副本; \(n\) 次結束時,慢一拍的正是 \(n-1\) 。在純粹的「只能前進」的宇宙裡,倒退是靠影子達成的——這類巧思正是無型別 λ 演算的迷人之處,也預告了第三章的主題:這些巧妙卻無規則可循的編碼,為何在加上型別後反而崩壞(教會數字在簡單型別 λ 演算中無法定義前驅!),從而逼出更強的型別系統。
章末小結#
- 布林、數字、條件、配對都能用純函數編碼:資料 = 它自己的解釋器。
- 教會數字把自然數定義為「疊代次數」,與皮亞諾結構及 fold 同構。
- \(F\ F\) 與 Y 組合子顯示:遞迴不是原語,而是 β 化約的定理;Ω 是發散的原型, \(Y\ F\) 是馴化的 Ω。
- 求值策略(正規 vs 應用順序)決定能否繞過無用的發散子項——這是 Haskell 惰性與 Python 急切的理論分野。
- λ 演算表達力與圖靈機等價;其自身亦有不可判定的停機問題。
下一步#
本章我們見證了無型別宇宙的蠻荒之力:x x 允許自指,Y 組合子允許遞迴,Ω 允許發散。但「什麼都能應用於什麼」也意味著沒有安全保障——(λx. x x) (λx. x x) 的型別在什麼意義上「不存在」?下一章(02.3)我們把整個數學搬進這個只有函數的宇宙:自然數系、算術、乃至更多的結構,全部由 λ 項構成。然後你將在第三章看到:當我們試圖給這個宇宙加上型別(回應第一章羅素悖論的同一份歷史直覺),Curry–Howard 同構的舞台就正式搭好了。
習題#
- 用 β 化約逐步驗證 \(\mathrm{AND}\ \mathrm{TRUE}\ \mathrm{FALSE} \to \mathrm{FALSE}\) 。
- 寫出教會數字的減法(提示: \(\mathrm{SUB} = \lambda m.\,\lambda n.\,n\ \mathrm{PRED}\ m\) ——把前驅疊 \(n\) 次),並用 Python 驗證 \(5 - 3\) 。
- 證明 \(Y\ F = F\ (Y\ F)\) 只在「允許無窮化約」的意義下成立;在急切求值的 Python 中,為何 Y 需要加上
lambda v: ...的包裝(eta 展開)才能運作? - 挑戰題:用教會編碼寫出
PAIR與FST/SND,並實作教會數字列表的「長度」函數。