1960-ALGOL 60 遞迴與傳名#
案件摘要#
1960 年 1 月,ALGOL 委員會在巴黎集會,由 Peter Naur 編輯的《Report on the Algorithmic Language ALGOL 60》問世,正式將「遞迴程序」(recursive procedures)與「傳名呼叫」(call-by-name)寫進國際標準語言規格。在此之前,遞迴在主流程式語言中是被明文禁止的禁忌——Fortran 與組合語言的世界裡,呼叫堆疊根本不存在。這份報告的幕後,是一場由數學遞迴函數傳統驅動的密謀:Dijkstra 在羅馬會議上力推遞迴程序,van Wijngaarden 則以一個關鍵範例說服眾人接受 call-by-name。本案追查:遞迴如何從禁忌變成標準,以及「每次使用都重新求值」的傳名語義,如何成為日後惰性求值(call-by-need)的直系祖先。
前因 -- 為什麼會有這個案子#
- Fortran(1957)與當時的組合語言均無遞迴:函數呼叫位址往往寫死在記憶體中,第二次呼叫會覆蓋自己的返回位址,遞迴等同自殺。
- IBM 甚至在文件中把 stack(堆疊)視為明確的禁忌話題,工程上普遍認為「不需要」遞迴。
- ALGOL 委員會的成員——Friedrich Bauer、Klaus Samelson、Heinz Rutishauser、Edsger Dijkstra——多出身歐陸數學傳統,熟悉 Gödel、Kleene 的遞迴函數理論,認為「程序應該能呼叫自己」是天經地義。
- Bauer 與 Samelson 早在 1957 年就為運算時的堆疊機制申請了專利("Verfahren zur automatischen Verarbeitung von kodierten Daten"),為遞迴的機器實現鋪好了地基。
- 1959/1960 年的羅馬(巴黎前) preparatory 會議上,Dijkstra 提案讓程序可以遞迴,遭到激烈反對;van Wijngaarden 以一個需要 call-by-name 的範例反將一軍,辯論白熱化。
- 委員會的目標是設計一門「演算法的通用語言」,既要能在機器上實現,又要夠數學、夠優雅——這兩種張力正是本案的動機。
線索與推理 -- 數學式、程式、理論#
線索一:call-by-name 的語義 = 不 memoize 的 β 歸約#
λ-Calculus 的 β 歸約是「把實參整個代入函數體,再求值」。call-by-name 正是這條規則的直接翻譯:每次用到參數,就重新求值一次實參表達式。
若實參 \(N\) 在函數體 \(M\) 中出現 \(k\) 次, \(N\) 就被求值 \(k\) 次——沒有任何記憶。相較之下,call-by-value 先求值 \(N\) 再傳入,只求值 1 次;call-by-need(惰性求值)第一次用到才求值,之後共用結果,也是 1 次。
線索二:複雜度分析——call-by-name 可能指數慢#
傳名的「每次重新求值」看似小毛病,實則可能指數爆炸。設 \(D_n = \lambda x.\,(x\,x)\) (自我複製器),考慮 \(A_n = D_n(D_{n-1}(\cdots D_1\; I))\) :每展開一層,實參被複製一份,總求值步數呈指數增長:
McDonald(Wadsworth 的導師圈常引述的 "amusement")與 Wadsworth 本人在 1971 年博士論文中正是用這類例子,證明 call-by-name 需要「共享」(sharing)來救——這就是 call-by-need 的誕生現場。
線索三:堆疊框與遞迴的機器實現#
遞迴可行的關鍵是每次呼叫都配置新的「活動記錄」(activation record / stack frame):返回位址、參數、局部變數都存在 frame 裡,frame 隨呼叫推入堆疊、隨返回彈出。Bauer-Samelson 1957 年的專利正是描述這套用堆疊自動處理巢狀資料的機制。有了它,遞迴只是「呼叫一個恰好是自己的函數」,沒有任何特別之處。
線索四:Jensen's Device——傳名的殺手級應用#
Jørgen Jensen 在委員會中提出的裝置,利用 call-by-name「每次使用重新求值」的特性,寫出一個參數化的求和迴圈:
real procedure SUM(i, lo, hi, u);
value lo, hi;
integer i, lo, hi; real u;
begin
real s;
s := 0;
for i := lo step 1 until hi do s := s + u;
SUM := s
end
呼叫 SUM(i, 1, 10, a[i] * 2) 時,u 是傳名參數:迴圈每迭代一次,就重新求值一次 a[i]*2,且 i 的改動會即時反映在 u 的求值中——一個程序,同時扮演 sum、sum of squares、任何推廣。這個近乎魔術的範例,讓反對者無法否認 call-by-name 的表達力。
程式示範:call-by-value vs call-by-name 的求值次數#
用 Python 的 thunk(零參數函數)模擬 call-by-name「每次重新執行」的行為:
counter = {"n": 0}
def expensive_body():
counter["n"] += 1
return 42
def cbv(thunk):
# call-by-value:先求值一次,之後共用
v = thunk()
return v + v + v
def cbn(thunk):
# call-by-name:每次使用都重新求值
return thunk() + thunk() + thunk()
print(cbv(expensive_body), counter["n"]) # 126 1 (求值 1 次)
print(cbn(expensive_body), counter["n"]) # 126 4 (又求值 3 次)
三處使用,call-by-value 共 1 次求值,call-by-name 共 4 次。把 v + v + v 換成巢狀展開,就是指數爆炸的縮影。
破案時刻#
1960 年報告第 4.7 節寫下 "recursive procedures" 一詞,附帶一句「程序可以透過自己呼叫自己」。Dijkstra 回憶,當委員會意識到 Bauer-Samelson 的堆疊機制讓遞迴「零成本」時,反對聲浪一夜瓦解——禁忌被邏輯處決。
結案 -- 後果與影響#
- 遞迴從禁忌變成標準配備;此後的語言若不支援遞迴,反而要特別聲明理由。
- call-by-name 的語義直接連到 λ-Calculus 的 β 歸約,成為惰性求值(call-by-need,Wadsworth 1971)的直接前身;Haskell 至今仍以 call-by-need 為核心。
- ALGOL 60 的 block structure(區塊結構、詞法作用域)直接影響 Pascal、C 與 Scheme——Scheme 的
let/lambda作用域就是 ALGOL 血統。 - Bauer-Samelson 的堆疊專利影響了日後所有編譯器的 runtime 設計(專利 1981 年到期,堆疊早已是公共財)。
- Jensen's Device 成為程式語言課程的經典案例,展示了「控制求值時機」本身就是表達力。
- ALGOL 60 被後世稱為「程式語言史上的重大改進」——但它自己從未成為主流商用語言,反倒是它的子孫統治了世界。
關鍵人物與文獻#
- Peter Naur (ed.), "Report on the Algorithmic Language ALGOL 60", Communications of the ACM 3(5), 1960, pp. 299–314.
- Edsger W. Dijkstra, "Recursive Programming", Numerische Mathematik 2, 1960, pp. 312–322.
- Friedrich L. Bauer & Klaus Samelson, "Verfahren zur automatischen Verarbeitung von kodierten Daten"(堆疊專利), 1957 年申請, US Patent 3,047,761(1962).
- Adriaan van Wijngaarden, 對 call-by-name 的羅馬會議提案,收錄於 ALGOL Bulletin 與 Naur 1960 報告的修訂史.
- Christopher Strachey, "Fundamental Concepts in Programming Languages", 1967 年講義, 收錄於 Higher-Order and Symbolic Computation 13, 2000, pp. 11–49(call-by-value / call-by-name 術語的定著點).
- David A. Turner, "Richard Bird and the History of Functional Programming", 及 J. E. Wadsworth, "Semantics and Pragmatics of the Lambda-Calculus", DPhil thesis, Oxford, 1971(call-by-need 的誕生).