1958:LISP 誕生#
事件#
1958年,John McCarthy 在 MIT 任職(繼 1956 年達特茅斯會議之後),著手設計一種適合符號運算與人工智慧研究的語言,即 LISP(LISt Processor)。同一年,第一個實作在 IBM 704 上完成——Steve Russell 意識到 McCarthy 論文中的 eval 可以直接寫成機器碼,於是實作了史上第一個 LISP 直譯器。LISP 是繼 FORTRAN(1957)之後第二古老的高階語言,也是至今仍存活的最古老高階語言。
語法/特性加入的理論與實用原因#
1. 列表與 S-表達式#
- 理論原因:AI 研究需要處理符號(符號、邏輯式、語句),而非 FORTRAN 擅長的數值運算。以遞迴定義的列表(element · list)是最自然的符號資料結構。
- 實用原因:程式與資料同構(homoiconicity)——程式本身就是列表,可以被當成資料操作,這讓「寫程式的程式」成為可能。
- 彌補缺陷:FORTRAN 與組合語言只能自然表達數值與位址運算,無法表達符號結構,AI 研究者被迫手寫組合語言處理列表。
(+) 1 2 3 ; 前綴記法的列表
'(a b (c d) e) ; 巢狀符號列表
對照組:要表達「某人的姓名與兩個電話」這種符號結構,LISP 一行即可;在 FORTRAN IV 裡卻必須宣告固定長度的字元陣列,符號的巢狀結構被迫壓平:
C FORTRAN IV:符號結構必須壓平成固定陣列
CHARACTER*20 NAME, PH1, PH2
NAME = 'TURING'
PH1 = '02-1234'
PH2 = '02-5678'
C 想再巢狀一層?只能另開陣列,自行記住對應關係
;; 同樣的資料,LISP 自然表達巢狀符號結構
'(person (name turing) (phones "02-1234" "02-5678"))
2. CAR/CDR#
- 理論原因:操作列表需要「取頭」與「取尾」兩個基本運算,對應列表的遞迴定義。
- 實用原因:名稱直接來自 IBM 704 硬體——CAR 是 Contents of the Address Register(位址暫存器內容),CDR 是 Contents of the Decrement Register(遞減暫存器內容)。硬體細節就這樣成為語言的一部分,並沿用至今。
CAR/CDR 加上 CONS(建構),就是列表的「解構與建構」兩面,遞迴處理由此展開:
(car '(a b c)) ; => A 取頭
(cdr '(a b c)) ; => (B C) 取尾
(car (cdr '(a b c))) ; => B 連用可達任何元素
(cadr '(a b c)) ; => B cadr 即 (car (cdr ...))
(cons 'a '(b c)) ; => (A B C) 把頭接回尾
(cons 1 (cons 2 nil)) ; => (1 2) 列表就是這樣一層層建起來的
3. 條件表達式(COND)#
- 理論原因:λ 演算本身沒有條件分支;McCarthy 在 1958–1960 年的論文中提出把條件做成「表達式」而非「敘述」,使其有值、可遞迴。
- 實用原因:遞迴函數需要明確的停止條件;條件表達式讓函數體可以用數學式的方式分支。
- 彌補缺陷:FORTRAN 的 IF 是敘述且不傳回值,無法寫出自然的遞迴數學定義。
(defun fact (n)
(cond ((= n 0) 1)
(t (* n (fact (- n 1))))))
COND 的每個子句是「條件、結果」成對出現,依序測試、命中即求值並傳回——本身就是一個有值的表達式:
;; COND 用途一:分支求值(可出現在表達式中任何位置)
(defun sign (n)
(cond ((= n 0) 'zero) ; 命中即傳回符號 ZERO
((> n 0) 'positive)
(t 'negative)))
(sign -5) ; => NEGATIVE
;; COND 用途二:遞迴的停止條件
(defun sum-list (l)
(cond ((null l) 0) ; 空列表:遞迴底
(t (+ (car l) (sum-list (cdr l))))))
(sum-list '(1 2 3 4)) ; => 10
對照組:FORTRAN II 的算術 IF 只能依「小於零/等於零/大於零」跳轉三個行號,不傳回值、不能寫遞迴,且每一層遞迴都得靠 GOTO 與手動堆疊模擬:
C FORTRAN II:條件是敘述,只能跳轉
IF (N) 10, 20, 30
10 STOP ; N < 0
20 RESULT = 1 ; N = 0
GOTO 40
30 ...
4. 遞迴作為主要控制結構#
- 理論原因:遞迴直接對應數學上的歸納定義,也對應 λ 演算的自我應用(透過 Y 組合子可處理遞迴)。
- 實用原因:列表是遞迴結構,用遞迴處理列表最為自然。
- 彌補缺陷:當時主流語言(FORTRAN)幾乎不支援遞迴,控制結構只有迴圈與跳轉。
費氏數列的遞迴定義幾乎是數學式的逐字翻譯:
;; 數學定義:F(0)=0, F(1)=1, F(n)=F(n-1)+F(n-2)
(defun fib (n)
(cond ((= n 0) 0)
((= n 1) 1)
(t (+ (fib (- n 1)) (fib (- n 2))))))
(fib 10) ; => 55
在 FORTRAN 裡,同樣的邏輯必須用迴圈與變數改寫成迭代(FORTRAN II 根本不能遞迴),而且中間值全靠變數管理:
C FORTRAN II:只能用迴圈與變數,無遞迴
IA = 0
IB = 1
DO 10 I = 1, 9
IT = IA + IB
IA = IB
IB = IT
10 CONTINUE
C 答案在 IB;三個變數的角色全靠程式設計師腦內追蹤
5. REPL 的發明#
- 實用原因:Steve Russell 的直譯器讓使用者可以逐式輸入、立即求值——這就是 Read-Eval-Print Loop(互動式迴圈)的誕生。LISP 也因此成為第一個支援互動式開發的語言,這在當時的批次處理世界是革命性的。
REPL 的互動示意(Read → Eval → Print 不斷循環):
> (+ 1 2)
3 ; 讀入、求值、印出,立即回應
> (defun fact (n) (cond ((= n 0) 1) (t (* n (fact (- n 1))))))
FACT ; 定義函數,傳回函數名
> (fact 5)
120 ; 馬上就能測試,不必走完整個編譯流程
> (fact 6)
720 ; 錯了?改一行再試——互動式開發由此誕生
在 1958 年,FORTRAN 的世界是批次處理:整疊打孔卡片交給電腦,跑完(或編譯錯誤中止)才知道結果,一來一回往往要等數小時。
彌補了什麼缺陷#
LISP 把 1936 年 λ 演算的理論帶進實用:函數可以遞迴、條件可以是表達式、程式可以是資料。它彌補了「高階語言無法表達符號運算與遞迴定義」的缺陷,成為此後數十年 AI 研究的標準載具,也開啟了整個函數式程式設計的實用史。