1960 Lambda 演算:函數的原子理論#
案發現場#
要理解 1960 年的「案發現場」,必須先回到更早的謎團。1930 年代,普林斯頓的 Alonzo Church 為了研究數學基礎,發明了 lambda 演算(λ-calculus)——一套只用「函數抽象」與「函數應用」兩種操作的極簡形式系統。1936 年 Church 用它證明了 Hilbert 的判定問題(Entscheidungsproblem)無解,與 Turing 的圖靈機證明遙相呼應。兩者後來被證明計算能力等價:Church-Turing 命題——一切可計算函數,都可用 lambda 演算計算。
但真正的「案發現場」在 1950 至 60 年代之交。當時程式語言界正面對幾個未解之謎:FORTRAN 與 COBOL 都是「命令式」語言,程式是一串改變狀態的指令;可是「函數」在這些語言裡只是二等公民——它不能被當成資料傳來傳去、不能在執行時期被組合出新的函數。數學家早已習慣把函數當值用( \(\sin\) 可以當成變數傳給 \(\int\) ),程式語言卻做不到。遇見這個問題的人包括 MIT 的 John McCarthy(LISP 之父)、以及研究 lambda 演算語義的 Haskell Curry 與 Peter Landin。他們追問:lambda 演算這套「函數的原子理論」,能否成為程式語言的數學基礎?
偵查過程#
線索一:λ x. x——一切從最簡單的函數開始#
Lambda 演算的項(term)用三條規則遞迴定義:
最簡單的例子 \(\lambda x.\,x\) 是恆等函數:輸入什麼就輸出什麼。注意整個系統不需要數字、不需要型別、不需要任何內建運算——一切都可以從函數建構出來。Church 甚至用純函數定義出了自然數(Church 數):
數字 \(n\) 的本質是「把函數 \(f\) 應用 \(n\) 次」——這是對「什麼是數」的深刻重新詮釋。加法也只是一個函數: \(\mathrm{add} = \lambda m.\,\lambda n.\,\lambda f.\,\lambda x.\; m\,f\,(n\,f\,x)\) ,它把 \(n\) 的「應用 \(f\) 共 \(n\) 次」再交給 \(m\) 繼續應用。整個算術,就用函數應用一招建構完成。
線索二:β 歸約與合流性——計算就是改寫#
Lambda 演算的「執行」只有一個動作:β 歸約(beta reduction)。把應用 \((\lambda x.\,M)\,N\) 改寫為把 \(M\) 中所有自由出現的 \(x\) 換成 \(N\) :
這正是函數呼叫的數學原型——程式語言的「呼叫函數、代入參數」就是 β 歸約的具體實現。但一個項可能有多處可歸約,先歸約哪裡?結果會不會不同?Church 與 Rosser 在 1936 年證明了 Church-Rosser 定理(合流性,confluence):若一個項可以歸約到兩個不同的正規形(normal form),則這兩個正規形必定相同(可在有限步內歸約到共同項)。用圖表示:
這個定理是函數式語言的理論基石:它保證了「求值順序不影響最終結果」(只要會終止),因此編譯器可以放心採用任何求值策略,甚至並行求值與惰性求值。Haskell 的惰性求值(call-by-need)之所以正確,正是站在 Church-Rosser 定理的肩膀上。
線索三:Y 組合子——不用名字的遞迴之謎#
Lambda 演算中最神秘的謎題是:函數匿名,如何遞迴? 遞迴需要函數呼叫自己,但 λ 抽象中的函數沒有名字,λf. f(f) 中的 f 指的是外面未定義的自由變數。解法是 Haskell Curry 找到的 Y 組合子:
其關鍵推導只有一行。令 \(A = \lambda x.\,f\,(x\,x)\) ,則:
也就是說 \(Y\,f = f\,(Y\,f)\) ——Y 組合子構造出了 \(f\) 的不動點:把 \(Y\,f\) 代入 \(f\) ,得到的還是 \(Y\,f\) 。遞迴因此誕生:把「階乘的下一步」 \(g = \lambda n.\; n \le 1\ ?\ 1 : n \times (\,\cdot\,)\,(n-1)\) 中的「自我」留一個洞,用 \(Y\,g\) 把 \(g\) 自己塞回那個洞,階乘就有定義了。這證明了一個驚人的事實:在純 lambda 演算中,遞迴不需要名字、不需要 assignment,只需要不動點。這也是為什麼 Scheme、Haskell 中遞迴的通用寫法是不動點組合子(Haskell 用 fix)。
線索四:Curry-Howard 對應#
Curry 與 Howard 在 1958–1969 年間發現了程式與邏輯的深層對應:
| 邏輯(自然演繹) | 程式(lambda 演算) |
|---|---|
| 命題 \(A\) | 型別 \(A\) |
| 證明 \(a: A\) | 程式項 \(a: A\) |
| 蘊含 \(A \Rightarrow B\) | 函數型別 \(A \to B\) |
| Modus ponens( \(a:A\) , \(f:A\to B\) 得 \(b:B\) ) | 函數應用 \(f\,a\) |
| \(\forall\) 量化 | 參數多型 |
一句話:命題即型別,證明即程式,證明化簡即程式求值。這個對應是後世 ML、Haskell、Coq、Agda 與依賴型別理論的理論地基。
結案報告#
Lambda 演算是整個函數式程式設計陣營的理論憲法:
- 1958 年 John McCarthy 的 LISP 直接把 lambda 演算的函數當成一等公民引入程式語言(
LAMBDA這個關鍵字沿用至今,Python 的lambda也是其後裔)。 - 1964 年 Peter Landin 用 SECD 機器證明 lambda 演算可以有效率地編譯執行,開啟了函數式語言的實作之路;ML 與 Haskell 的型別推導、惰性求值都源於此。
- Scheme 的詞彙作用域與一級函數、JavaScript 的閉包與箭頭函數
x => x(正是 \(\lambda x.\,x\) 的符號化身)、Java 8 的 lambda 表達式,全部都是 lambda 演算的語言學遺產。 - Curry-Howard 對應催生了證明助理(Coq、Agda、Lean),今日數學定理的機器驗證直接建築在 lambda 演算之上。
- 遞迴之謎的解答(不動點)也影響了程式語義學:Scott 以領域理論(domain theory)給 lambda 演算建立數學模型,為 1968 年 Dijkstra 之後的形式化驗證運動提供了工具。
一句話總結:圖靈機是「機器」的原子理論,lambda 演算是「函數」的原子理論;前者造出了硬體,後者造出了函數式語言。
證據與工具#
以下用 Python 實作一個迷你 lambda 演算直譯器,重現 β 歸約、Church 數與 Y 組合子:
# 迷你 lambda 演算:Var / App / Lam 三種項
class Var:
def __init__(self, name): self.name = name
class Lam: # Lam(param, body) = λparam.body
def __init__(self, param, body): self.param, self.body = param, body
class App: # App(f, arg) = f arg
def __init__(self, f, arg): self.f, self.arg = f, arg
def free_vars(t):
if isinstance(t, Var): return {t.name}
if isinstance(t, Lam): return free_vars(t.body) - {t.param}
return free_vars(t.f) | free_vars(t.arg)
def subst(t, name, val): # 把 t 中自由的 name 換成 val(β 歸約的核心)
if isinstance(t, Var): return val if t.name == name else t
if isinstance(t, Lam):
if t.param == name: return t
# 避免捕捉:改名(α 變換的簡化處理,此處省略衝突情況)
return Lam(t.param, subst(t.body, name, val))
return App(subst(t.f, name, val), subst(t.arg, name, val))
def reduce(t, depth=0): # β 歸約到正規形(Church-Rosser 保證結果唯一)
if isinstance(t, App) and isinstance(t.f, Lam):
return reduce(subst(t.f.body, t.f.param, t.arg))
if isinstance(t, Lam):
return Lam(t.param, reduce(t.body))
return t
show = lambda t: (
t.name if isinstance(t, Var) else
f"(λ{t.param}.{show(t.body)})" if isinstance(t, Lam) else
f"({show(t.f)} {show(t.arg)})")
# 恆等函數 λx.x 應用於 5
I = Lam("x", Var("x"))
print("β 歸約 (λx.x 5) ->", show(reduce(App(I, Var("5")))))
# Church 數:0 = λf.λx.x , 2 = λf.λx.f(f x)
c0 = Lam("f", Lam("x", Var("x")))
c2 = Lam("f", Lam("x", App(Var("f"), App(Var("f"), Var("x")))))
succ = Lam("n", Lam("f", Lam("x", App(Var("f"), App(App(Var("n"), Var("f")), Var("x"))))))
c3 = reduce(App(succ, c2))
print("succ(2) 的 Church 數:", show(c3))
# Church 數 3 展開:把 f = (λf.λx.f x) 應用 3 次
val = App(App(c3, Lam("f", Lam("x", App(Var("f"), Var("x"))))), Var("x"))
print("Church 數 3 展開:", show(reduce(val)))
# Y 組合子:Y = λf.(λx.f(x x))(λx.f(x x))
Y = Lam("f", App(Lam("x", App(Var("f"), App(Var("x"), Var("x")))),
Lam("x", App(Var("f"), App(Var("x"), Var("x"))))))
print("Y 的結構:", show(Y))
# 用 Python 原生 lambda 模擬 Y f = f (Y f) 的不動點性質
Yf = lambda f: (lambda x: f(x(x)))(lambda x: f(x(x)))
# 有限版本的惰性 Y(避免無限展開):用 Python 的 lambda 模擬 call-by-need
def lazyY(f):
return (lambda x: f(lambda *a: x(x)(*a)))(lambda x: f(lambda *a: x(x)(*a)))
fact_gen = lambda f: lambda n: 1 if n <= 1 else n * f(n - 1)
factorial = lazyY(fact_gen)
print("Y 組合子實作遞迴:5! =", factorial(5), ", 10! =", factorial(10))
執行結果顯示三件事:第一,β 歸約把 (λx.x 5) 化為 5,函數呼叫就是改寫;第二,Church 數 succ(2) 展開為 λf.λx.f(f(f x)),數字就是函數應用的次數;第三,lazyY 用純 lambda(Python 的匿名函數)構造出階乘遞迴,證明遞迴不需要名字——只需要不動點 \(Y\,f = f\,(Y\,f)\) 。