Lisp之根源 zz

保羅格雷厄姆

約翰麥卡錫於1960年發表了一篇非凡的論文,他在這篇論文中對編程的貢獻有如歐幾里德對幾何的貢獻.1 他向我們展示了,在只給定幾個簡單的操作符和一個表示函數的記號的基礎上, 如何構造出一個完整的編程語言. 麥卡錫稱這種語言爲Lisp, 意爲List Processing, 因爲他的主要思想之一是用一種簡單的數據結構表(list)來代表代碼和數據.


值 得注意的是,麥卡錫所作的發現,不僅是計算機史上劃時代的大事, 而且是一種在我們這個時代編程越來越趨向的模式.我認爲目前爲止只有兩種真正乾淨利落, 始終如一的編程模式:C語言模式和Lisp語言模式.此二者就象兩座高地, 在它們中間是尤如沼澤的低地.隨着計算機變得越來越強大,新開發的語言一直在堅定地趨向於Lisp模式. 二十年來,開發新編程語言的一個流行的祕決是,取C語言的計算模式,逐漸地往上加Lisp模式的特性,例如運行時類型和無用單元收集.

在 這篇文章中我儘可能用最簡單的術語來解釋約翰麥卡錫所做的發現. 關鍵是我們不僅要學習某個人四十年前得出的有趣理論結果, 而且展示編程語言的發展方向. Lisp的不同尋常之處--也就是它優質的定義--是它能夠自己來編寫自己. 爲了理解約翰麥卡錫所表述的這個特點,我們將追溯他的步伐,並將他的數學標記轉換成能夠運行的Common Lisp代碼.

七個原始操作符

開始我們先定義表達式.表達式或是一個原子(atom),它是一個字母序列(如 foo),或是一個由零個或多個表達式組成的(list), 表達式之間用空格分開, 放入一對括號中. 以下是一些表達式:


最後一個表達式是由四個元素組成的表, 第三個元素本身是由一個元素組成的表. 

在算術中表達式 1 + 1 得出值2. 正確的Lisp表達式也有值. 如果表達式e得出值v,我們說e返回v. 下一步我們將定義幾種表達式以及它們的返回值.

如果一個表達式是表,我們稱第一個元素爲操作符,其餘的元素爲自變量.我們將定義七個原始(從公理的意義上說)操作符: quote,atom,eq,car,cdr,cons,和 cond.

  1. (quote x) 返回x.爲了可讀性我們把(quote x)簡記 爲'x.
    
    
  2. (atom x)返回原子t如果x的值是一個原子或是空表,否則返回(). 在Lisp中我們按慣例用原子t表示真, 而用空表表示假.
    
    

    既然有了一個自變量需要求值的操作符, 我們可以看一下quote的作用. 通過引用(quote)一個表,我們避免它被求值. 一個未被引用的表作爲自變量傳給象 atom這樣的操作符將被視爲代碼:

    
    

    反之一個被引用的表僅被視爲表, 在此例中就是有兩個元素的表:

    
    

    這與我們在英語中使用引號的方式一致. Cambridge(劍橋)是一個位於麻薩諸塞州有90000人口的城鎮. 而``Cambridge''是一個由9個字母組成的單詞.

    引用看上去可能有點奇怪因爲極少有其它語言有類似的概念. 它和Lisp最與衆不同的特徵緊密聯繫:代碼和數據由相同的數據結構構成, 而我們用quote操作符來區分它們.

  3. (eq x y)返回t如果xy的值是同一個原子或都是空表, 否則返回().
    
    
  4. (car x)期望x的值是一個表並且返回x的第一個元素.
    
    
  5. (cdr x)期望x的值是一個表並且返回x的第一個元素之後的所有元素.
    
    
  6. (cons x y)期望y的值是一個表並且返回一個新表,它的第一個元素是x的值, 後面跟着y的值的各個元素.
    
    
  7. (cond ($p_{1}$...$e_{1}$) ...($p_{n}$...$e_{n}$)) 的求值規則如下. p表達式依次求值直到有一個返回t. 如果能找到這樣的p表達式,相應的e表達式的值作爲整個cond表達式的返回值.
    
    

    當表達式以七個原始操作符中的五個開頭時,它的自變量總是要求值的.2 我們稱這樣 的操作符爲函數.

函數的表示

接着我們定義一個記號來描述函數.函數表示爲(lambda ($p_{1}$...$p_{n}$) e),其中 $p_{1}$...$p_{n}$是原子(叫做參數),e是表達式. 如果表達式的第一個元素形式如上

((lambda ($p_{1}$...$p_{n}$) e) $a_{1}$...$a_{n}$)

則稱爲函數調用.它的值計算如下.每一個表達式$a_{i}$先求值,然後e再求值.在e的求值過程中,每個出現在e中的$p_{i}$的值是相應的$a_{i}$在最近一次的函數調用中的值.


如果一個表達式的第一個元素f是原子且f不是原始操作符 

(f $a_{1}$...$a_{n}$)

並且f的值是一個函數(lambda ($p_{1}$...$p_{n}$)),則以上表達式的值就是

((lambda ($p_{1}$...$p_{n}$) e) $a_{1}$...$a_{n}$)

的值. 換句話說,參數在表達式中不但可以作爲自變量也可以作爲操作符使用:


有另外一個函數記號使得函數能提及它本身,這樣我們就能方便地定義遞歸函數.3 記號

(label f (lambda ($p_{1}$...$p_{n}$) e))

表示一個象(lambda ($p_{1}$...$p_{n}$) e)那樣的函數,加上這樣的特性: 任何出現在e中的f將求值爲此label表達式, 就好象f是此函數的參數.

假設我們要定義函數(subst x y z), 它取表達式x,原子y和表z做參數,返回一個象z那樣的表, 不過z中出現的y(在任何嵌套層次上)被x代替.


我們可以這樣表示此函數 

我們簡記f=(label f (lambda ($p_{1}$...$p_{n}$) e))爲 

(defun f ($p_{1}$...$p_{n}$) e)

於是


偶然地我們在這兒看到如何寫cond表達式的缺省子句. 第一個元素是't的子句總是會成功的. 於是 

(cond (x y) ('t z))

等同於我們在某些語言中寫的

if x then y else z

一些函數

既然我們有了表示函數的方法,我們根據七個原始操作符來定義一些新的函數. 爲了方便我們引進一些常見模式的簡記法. 我們用cxr,其中x是a或d的序列,來簡記相應的car和cdr的組合. 比如(cadr e)是(car (cdr e))的簡記,它返回e的第二個元素.

我們還用(list $e_{1}$...$e_{n}$)表示(cons $e_{1}$...(cons $e_{n}$'()) ...). 

現在我們定義一些新函數. 我在函數名後面加了點,以區別函數和定義它們的原始函數,也避免與現存的common Lisp的函數衝突.

  1. (null. x)測試它的自變量是否是空表.
    
    
  2. (and. x y)返回t如果它的兩個自變量都是t, 否則返回().
    
    
  3. (not. x)返回t如果它的自變量返回(),返回()如果它的自變量返回t.
    
    
  4. (append. x y)取兩個表並返回它們的連結.
    
    
  5. (pair. x y)取兩個相同長度的表,返回一個由雙元素表構成的表,雙元素表是相應位置的x,y的元素對.
    
    
  6. (assoc. x y)取原子x和形如pair.函數所返回的表y,返回y中第一個符合如下條件的表的第二個元素:它的第一個元素是x.
    
    

一個驚喜

因此我們能夠定義函數來連接表,替換表達式等等.也許算是一個優美的表示法, 那下一步呢? 現在驚喜來了. 我們可以寫一個函數作爲我們語言的解釋器:此函數取任意Lisp表達式作自變量並返回它的值. 如下所示:

eval.的定義比我們以前看到的都要長. 讓我們考慮它的每一部分是如何工作的. 

eval.有兩個自變量: e是要求值的表達式, a是由一些賦給原子的值構成的表,這些值有點象函數調用中的參數. 這個形如pair.的返回值的表叫做環境. 正是爲了構造和搜索這種表我們才寫了pair.和assoc..

eval.的骨架是一個有四個子句的cond表達式. 如何對表達式求值取決於它的類型. 第一個子句處理原子. 如果e是原子, 我們在環境中尋找它的值:


第二個子句是另一個cond, 它處理形如(a ...)的表達式, 其中a是原子. 這包括所有的原始操作符, 每個對應一條子句.


這幾個子句(除了quote)都調用eval.來尋找自變量的值. 

最後兩個子句更復雜些. 爲了求cond表達式的值我們調用了一個叫 evcon.的輔助函數. 它遞歸地對cond子句進行求值,尋找第一個元素返回t的子句. 如果找到了這樣的子句, 它返回此子句的第二個元素.


第二個子句的最後部分處理函數調用. 它把原子替換爲它的值(應該是lambda 或label表達式)然後對所得結果表達式求值. 於是


變爲 

它返回(a b c). 

eval.的最後cond兩個子句處理第一個元素是lambda或label的函數調用.爲了對label 表達式求值, 先把函數名和函數本身壓入環境, 然後調用eval.對一個內部有 lambda的表達式求值. 即:


變爲 

最終返回a. 

最後,對形如((lambda ($p_{1}$...$p_{n}$) e) $a_{1}$...$a_{n}$)的表達式求值,先調用evlis.來求得自變量($a_{1}$...$a_{n}$)對應的值($v_{1}$...$v_{n}$),把($p_{1}$$v_{1}$)...($p_{n}$$v_{n}$)添加到環境裏, 然後對e求值. 於是


變爲 

最終返回(a c d). 

後果

既 然理解了eval是如何工作的, 讓我們回過頭考慮一下這意味着什麼. 我們在這兒得到了一個非常優美的計算模型. 僅用quote,atom,eq,car,cdr,cons,和cond, 我們定義了函數eval.,它事實上實現了我們的語言,用它可以定義任何我們想要的額外的函數.

當然早已有了各種計算模型--最著名的是圖靈機. 但是圖靈機程序難以讀懂. 如果你要一種描述算法的語言, 你可能需要更抽象的, 而這就是約翰麥卡錫定義 Lisp的目標之一.

約翰麥卡錫於1960年定義的語言還缺不少東西. 它沒有副作用, 沒有連續執行 (它得和副作用在一起纔有用), 沒有實際可用的數,4 沒有動態可視域. 但這些限制可以令人驚訝地用極少的額外代碼來補救. Steele和Sussman在一篇叫做``解釋器的藝術''的著名論文中描述瞭如何做到這點.5

如 果你理解了約翰麥卡錫的eval, 那你就不僅僅是理解了程序語言歷史中的一個階段. 這些思想至今仍是Lisp的語義核心. 所以從某種意義上, 學習約翰麥卡錫的原著向我們展示了Lisp究竟是什麼. 與其說Lisp是麥卡錫的設計,不如說是他的發現. 它不是生來就是一門用於人工智能, 快速原型開發或同等層次任務的語言. 它是你試圖公理化計算的結果(之一).

隨着時間的推移, 中級語言, 即被中間層程序員使用的語言, 正一致地向Lisp靠近. 因此通過理解eval你正在明白將來的主流計算模式會是什麼樣.

註釋

把約翰麥卡錫的記號翻譯爲代碼的過程中我儘可能地少做改動. 我有過讓代碼更容易閱讀的念頭, 但是我還是想保持原汁原味.

在約翰麥卡錫的論文中,假用f來表示, 而不是空表. 我用空表表示假以使例子能在Common Lisp中運行. (fixme)

我 略過了構造dotted pairs, 因爲你不需要它來理解eval. 我也沒有提apply, 雖然是apply(它的早期形式, 主要作用是引用自變量), 被約翰麥卡錫在1960年稱爲普遍函數, eval只是不過是被apply調用的子程序來完成所有的工作.

我定義了list和cxr等作爲簡記法因爲麥卡錫就是這麼做的. 實際上 cxr等可以被定義爲普通的函數. List也可以這樣, 如果我們修改eval, 這很容易做到, 讓函數可以接受任意數目的自變量.

麥卡錫的論文中只有五個原始操作符. 他使用了cond和quote,但可能把它們作爲他的元語言的一部分. 同樣他也沒有定義邏輯操作符and和not, 這不是個問題, 因爲它們可以被定義成合適的函數.

在eval.的定義中我們調用了其它函數如pair.和assoc.,但任何我們用原始操作符定義的函數調用都可以用eval.來代替. 即


能寫成 

麥卡錫的eval有一個錯誤. 第16行是(相當於)(evlis. (cdr e) a)而不是(cdr e), 這使得自變量在一個有名函數的調用中被求值兩次. 這顯示當論文發表的時候, eval的這種描述還沒有用IBM 704機器語言實現. 它還證明了如果不去運行程序, 要保證不管多短的程序的正確性是多麼困難.

我還在麥卡錫的論文中碰到一個問題. 在定義了eval之後, 他繼續給出了一些更高級的函數--接受其它函數作爲自變量的函數. 他定義了maplist:


然後用它寫了一個做微分的簡單函數diff. 但是diff傳給maplist一個用x做參數的函數, 對它的引用被maplist中的參數x所捕獲.6 

這 是關於動態可視域危險性的雄辯證據, 即使是最早的更高級函數的例子也因爲它而出錯. 可能麥卡錫在1960年還沒有充分意識到動態可視域的含意. 動態可視域令人驚異地在Lisp實現中存在了相當長的時間--直到Sussman和Steele於 1975年開發了Scheme. 詞法可視域沒使eval的定義複雜多少, 卻使編譯器更難寫了.

About this document ...

Lisp之根源

This document was generated using the LaTeX2HTML translator Version 2K.1beta (1.48)

Copyright © 1993, 1994, 1995, 1996, Nikos Drakos, Computer Based Learning Unit, University of Leeds.
Copyright © 1997, 1998, 1999,
Ross Moore, Mathematics Department, Macquarie University, Sydney.

The command line arguments were:
latex2html -split=0 roots_of_lisp.tex

The translation was initiated by Dai Yuwen on 2003-10-24

Footnotes

... 歐幾里德對幾何的貢獻.1
``Recursive Functions of Symbolic Expressions and Their Computation by Machine, Part1.'' Communication of the ACM 3:4, April 1960, pp. 184-195.
...當表達式以七個原始操作符中的五個開頭時,它的自變量總是要求值的.2
以另外兩個操作符quote和cond開頭的表達式以不同的方式求值. 當 quote表達式求值時, 它的自變量不被求值,而是作爲整個表達式的值返回. 在 一個正確的cond表達式中, 只有L形路徑上的子表達式會被求值.
... 數.3
邏輯上我們不需要爲了這定義一個新的記號. 在現有的記號中用 一個叫做Y組合器的函數上的函數, 我們可以定義遞歸函數. 可能麥卡錫在寫 這篇論文的時候還不知道Y組合器; 無論如何, label可讀性更強.
... 沒有實際可用的數,4
在麥卡錫的1960 年的Lisp中, 做算術是可能的, 比如用一個有n個原子的表表示數n.
... 的藝術''的著名論文中描述瞭如何做到這點.5
Guy Lewis Steele, Jr. and Gerald Jay Sussman, ``The Art of the Interpreter, or the Modularity Complex(Parts Zero,One,and Two),'' MIT AL Lab Memo 453, May 1978.
... 對它的引用被maplist中的參數x所捕獲.6
當代的Lisp程序 員在這兒會用mapcar代替maplist. 這個例子解開了一個謎團: maplist爲什 麼會在Common Lisp中. 它是最早的映射函數, mapcar是後來增加的.

轉載自:http://daiyuwen.freeshell.org/gb/rol/roots_of_lisp.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章