1975-Scheme與Lambda終極系列#
案件摘要#
1975 年,Gerald Sussman 與 Guy Steele 在 MIT AI Lab 創造了 Scheme——史上最極簡的 Lisp 方言。Steele 隨後在 1975-1978 年間發表「Lambda: the Ultimate GOTO」等系列論文(AI Memo 443 等),證明了一個驚人的命題:函數呼叫就是 goto,尾呼叫不需要增長堆疊。加上詞法範疇、continuation-passing style 與 call/cc,Scheme 把「控制流」從機器細節提升為一等公民的數學物件,深刻影響了此後所有語言的設計。
前因 -- 為什麼會有這個案子#
- 1970 年代初,MacLisp 已經太龐雜:幾十個 special forms、動態範疇、各種歷史包袱,不適合做語言實驗。
- Sussman 當時在研究 Carl Hewitt 的 actor 模型(1973),想弄清楚「actor 模型 vs closure」的真正差異是什麼——兩者看起來都像「帶行為的物件」。
- 他需要一個最小 Lisp 來做實驗:把不必要的機制全部砍掉,只留最核心的幾個構造。
- Steele 加入後,兩人在幾個月內寫出了第一版 Scheme 直譯器,意外發現了一個理論金礦:如果函數呼叫永遠不增長 stack,那「呼叫」與「跳躍」在語義上無法區分。
- 破案時刻:Steele 意識到這不只是實作技巧,而是一個理論命題——lambda 是 ultimate goto,於是寫成系列論文。
線索與推理 -- 數學式、程式、理論#
線索一:極簡 Lisp#
Scheme 的設計原則:語法只保留最少的 special forms——lambda、if、define、quote,其餘一切都是函數或 macro。第一版直譯器只有幾十行。
線索二:詞法範疇(lexical scoping)#
與 MacLisp 的動態範疇不同,Scheme 的變數查 lookup 由程式文本的靜態結構決定:
(define (make-adder n)
(lambda (x) (+ x n))) ; n 由定義處的詞法環境捕捉
(define add5 (make-adder 5))
(add5 3) ; => 8
這讓 lambda 真正成為「閉包」(closure):函數值捕捉其定義環境,行為可預測、可數學推論。
線索三:適當尾端呼叫(proper tail call)-- 破案時刻#
尾呼叫(tail call)是函數體的最後一個操作。Steele 證明:尾呼叫不需要保留呼叫者的堆疊框,因此不增長 stack:
; 尾遞迴:最後的操作是呼叫自己,stack 不增長
(define (fact n acc)
(if (= n 0)
acc
(fact n (- n 1) (* n acc)))) ; 尾呼叫
(fact 100000 1) ; 不會 stack overflow
實作上,直譯器把尾呼叫編譯成跳躍:
# Python 模擬尾呼叫優化:trampoline
def trampoline(f, *args):
while callable(result := f(*args)):
args = result[1:] if isinstance(result, tuple) else ()
f = result[0] if isinstance(result, tuple) else result
return result
def fact_trampoline(n, acc=1):
if n == 0:
return acc # 返回真值,跳出 trampoline
return (fact_trampoline, n - 1, n * acc) # 返回「跳躍目標」
print(trampoline(fact_trampoline, 100000)) # 算得出 100000!
「函數呼叫 = goto」的意義:尾呼叫優化讓迴圈只是尾遞迴的特例,控制結構可以全部用函數呼叫表達。
線索四:continuation-passing style(CPS)#
CPS 是把控制流顯式化的變換:每個函數多一個參數 k(continuation,代表「接下來要做什麼」),所有呼叫都是尾呼叫:
; CPS 版本的 fact:k 是 continuation
(define (fact/k n k)
(if (= n 0)
(k 1) ; 尾呼叫
(fact/k (- n 1)
(lambda (v) (k (* n v)))))) ; 也是尾呼叫
(fact/k 5 (lambda (x) x)) ; => 120
CPS 中控制流完全由 continuation 參數決定,堆疊變成堆上的資料結構。這成為編譯器中間表示的基礎。
線索五:call/cc -- 一等公民續體#
Scheme 的 call-with-current-continuation(call/cc)把「當前 continuation」做成一等公民的值:
; call/cc 實作 non-local exit
(define (find-first pred lst)
(call/cc
(lambda (return)
(for-each (lambda (x)
(when (pred x)
(return x))) ; 跳出整個 for-each
lst)
#f)))
(find-first even? '(1 3 5 6 7)) ; => 6
continuation 是「程式剩餘部分」的具體化:呼叫它就跳到那個時刻繼續執行。這是控制流的最一般理論。
線索六:R5RS 的 define-syntax macro#
後續 Scheme 標準(R5RS, 1998)加入衛生 macro(hygienic macro)define-syntax,讓使用者可以用語法規則擴充語言,同時不破壞詞法範疇。
結案 -- 後果與影響#
- Scheme 成為 MIT 6.001 課程教材的基礎,催生經典教科書《Structure and Interpretation of Computer Programs》(SICP, Abelson 與 Sussman, 1985)。
- Racket(前 PLT Scheme)、Guile(GNU 的延伸語言)都是 Scheme 直系後裔。
- proper tail call 進入 ECMAScript 6 標準(實作於 JavaScriptCore/Safari),成為語言標準的一部分。
- CPS 成為編譯器中間表示:Andrew Appel 的 CPS 變換、GHC 的 STG(Spineless Tagless G-machine)皆為其後裔。
- continuation 影響 async/await:JavaScript、C#、Python 的 async/await 語法本質上是有限版本的 continuation 機制。
- 詞法範疇成為所有現代語言的標準;actor 模型的疑惑也促成了 Sussman 對「物件導向 vs 函數式」的持續思考。
關鍵人物與文獻#
- Steele, G. L. and Sussman, G. J., Scheme: An Interpreter for Extended Lambda Calculus, MIT AI Memo 349, 1975。
- Steele, G. L., Debunking the "Expensive Procedure Call" Myth or, Procedure Call Implementations Reconsidered, MIT AI Memo 443, 1977。
- Steele, G. L., Lambda: The Ultimate GOTO, Proceedings of the ACM Conference on AI and Programming Languages, 1977。
- Steele, G. L., Lambda: The Ultimate GOTO 系列後續:Lambda: The Ultimate Declarative (AI Memo 379)、The Art of the Interpreter (Sussman and Steele, AI Memo 453, 1978)。
- Abelson, H. and Sussman, G. J., Structure and Interpretation of Computer Programs, MIT Press, 1985。
- Kelsey, R., Clinger, W., Rees, J. (eds.), Revised^5 Report on the Algorithmic Language Scheme (R5RS), 1998。
- Hewitt, C., Bishop, P., Steiger, R., A Universal Modular ACTOR Formalism for Artificial Intelligence, IJCAI, 1973。