LISP之根源
約翰麥卡錫于1960年發表了一篇非凡的論文,他在這篇論文中對編程的貢獻有如歐幾里德對幾何的貢獻.1 他向我們展示了,在只給定幾個簡單的操作符和一個表示函數的記號的基礎上, 如何構造出一個完整的編程語言. 麥卡錫稱這種語言為Lisp, 意為List Processing, 因為他的主要思想之一是用一種簡單的數據結構表(list)來代表代碼和數據.
值得注意的是,麥卡錫所作的發現,不僅是計算機史上劃時代的大事, 而且是一種在我們這個時代編程越來越趨向的模式.我認為目前為止只有兩種真正干凈利落, 始終如一的編程模式:C語言模式和Lisp語言模式.此二者就象兩座高地, 在它們中間是尤如沼澤的低地.隨著計算機變得越來越強大,新開發的語言一直在堅定地趨向于Lisp模式. 二十年來,開發新編程語言的一個流行的秘決是,取C語言的計算模式,逐漸地往上加Lisp模式的特性,例如運行時類型和無用單元收集.
在這篇文章中我盡可能用最簡單的術語來解釋約翰麥卡錫所做的發現. 關鍵是我們不僅要學習某個人四十年前得出的有趣理論結果, 而且展示編程語言的發展方向. Lisp的不同尋常之處--也就是它優質的定義--是它能夠自己來編寫自己. 為了理解約翰麥卡錫所表述的這個特點,我們將追溯他的步伐,并將他的數學標記轉換成能夠運行的Common Lisp代碼.
七個原始操作符
開始我們先定義表達式.表達式或是一個原子(atom),它是一個字母序列(如 foo),或是一個由零個或多個表達式組成的表(list), 表達式之間用空格分開, 放入一對括號中. 以下是一些表達式:
foo()
(foo)
(foo?bar)
(a?b?(c)?d) 最后一個表達式是由四個元素組成的表, 第三個元素本身是由一個元素組成的表.
在算術中表達式 1 + 1 得出值2. 正確的Lisp表達式也有值. 如果表達式e得出值v,我們說e返回v. 下一步我們將定義幾種表達式以及它們的返回值.
如果一個表達式是表,我們稱第一個元素為操作符,其余的元素為自變量.我們將定義七個原始(從公理的意義上說)操作符: quote,atom,eq,car,cdr,cons,和 cond.
a
>?'a
a
>?(quote?(a?b?c))
(a?b?c)
t
>?(atom?'(a?b?c))
()
>?(atom?'())
t
既然有了一個自變量需要求值的操作符, 我們可以看一下quote的作用. 通過引用(quote)一個表,我們避免它被求值. 一個未被引用的表作為自變量傳給象 atom這樣的操作符將被視為代碼:
>?(atom?(atom?'a))t
反之一個被引用的表僅被視為表, 在此例中就是有兩個元素的表:
>?(atom?'(atom?'a))()
這與我們在英語中使用引號的方式一致. Cambridge(劍橋)是一個位于麻薩諸塞州有90000人口的城鎮. 而``Cambridge''是一個由9個字母組成的單詞.
引用看上去可能有點奇怪因為極少有其它語言有類似的概念. 它和Lisp最與眾不同的特征緊密聯系:代碼和數據由相同的數據結構構成, 而我們用quote操作符來區分它們.
t
>?(eq?'a?'b)
()
>?(eq?'()?'())
t
a
(b?c)
(a?b?c)
>?(cons?'a?(cons?'b?(cons?'c?'())))
(a?b?c)
>?(car?(cons?'a?'(b?c)))
a
>?(cdr?(cons?'a?'(b?c)))
(b?c)
????????((atom?'a)??'second))
second
當表達式以七個原始操作符中的五個開頭時,它的自變量總是要求值的.2 我們稱這樣 的操作符為函數.
函數的表示
接著我們定義一個記號來描述函數.函數表示為(lambda (p1) e),其中 p1是原子(叫做參數),e是表達式. 如果表達式的第一個元素形式如上((lambda (p1) e) a1...an)
則稱為函數調用.它的值計算如下.每一個表達式ai先求值,然后e再求值.在e的求值過程中,每個出現在e中的pi的值是相應的ai在最近一次的函數調用中的值.
>?((lambda?(x)?(cons?x?'(b)))?'a)(a?b)
>?((lambda?(x?y)?(cons?x?(cdr?y)))
???'z
???'(a?b?c))
(z?b?c) 如果一個表達式的第一個元素f是原子且f不是原始操作符
(f a1...an)
并且f的值是一個函數(lambda (p1)),則以上表達式的值就是
((lambda (p1) e) a1...an)
的值. 換句話說,參數在表達式中不但可以作為自變量也可以作為操作符使用:
>?((lambda?(f)?(f?'(b?c)))???'(lambda?(x)?(cons?'a?x)))
(a?b?c)
有另外一個函數記號使得函數能提及它本身,這樣我們就能方便地定義遞歸函數.3 記號
(label f (lambda (p1) e))
表示一個象(lambda (p1) e)那樣的函數,加上這樣的特性: 任何出現在e中的f將求值為此label表達式, 就好象f是此函數的參數.
假設我們要定義函數(subst x y z), 它取表達式x,原子y和表z做參數,返回一個象z那樣的表, 不過z中出現的y(在任何嵌套層次上)被x代替.
>?(subst?'m?'b?'(a?b?(a?b?c)?d))(a?m?(a?m?c)?d) 我們可以這樣表示此函數 (label?subst?(lambda?(x?y?z)
???????????????(cond?((atom?z)
??????????????????????(cond?((eq?z?y)?x)
????????????????????????????('t?z)))
?????????????????????('t?(cons?(subst?x?y?(car?z))
???????????????????????????????(subst?x?y?(cdr?z))))))) 我們簡記f=(label f (lambda (p1) e))為
(defun f (p1) e)
于是
(defun?subst?(x?y?z)??(cond?((atom?z)
?????????(cond?((eq?z?y)?x)
???????????????('t?z)))
????????('t?(cons?(subst?x?y?(car?z))
??????????????????(subst?x?y?(cdr?z)))))) 偶然地我們在這兒看到如何寫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的第二個元素. >?(cadr?'((a?b)?(c?d)?e))(c?d)
>?(caddr?'((a?b)?(c?d)?e))
e
>?(cdar?'((a?b)?(c?d)?e))
(b) 我們還用(list e1...en)表示(cons e1...(cons en'()) ...). >?(cons?'a?(cons?'b?(cons?'c?'())))
(a?b?c)
>?(list?'a?'b?'c)
(a?b?c)
現在我們定義一些新函數. 我在函數名后面加了點,以區別函數和定義它們的原始函數,也避免與現存的common Lisp的函數沖突.
??(eq?x?'()))
>?(null.?'a)
()
>?(null.?'())
t
??(cond?(x?(cond?(y?'t)?('t?'())))
????????('t?'())))
>?(and.?(atom?'a)?(eq?'a?'a))
t
>?(and.?(atom?'a)?(eq?'a?'b))
()
??(cond?(x?'())
????????('t?'t)))
>?(not.?(eq?'a?'a))
()
>?(not.?(eq?'a?'b))
t
???(cond?((null.?x)?y)
?????????('t?(cons?(car?x)?(append.?(cdr?x)?y)))))
>?(append.?'(a?b)?'(c?d))
(a?b?c?d)
>?(append.?'()?'(c?d))
(c?d)
??(cond?((and.?(null.?x)?(null.?y))?'())
????????((and.?(not.?(atom?x))?(not.?(atom?y)))
?????????(cons?(list?(car?x)?(car?y))
???????????????(pair.?(cdr)?(cdr?y))))))
>?(pair.?'(x?y?z)?'(a?b?c))
((x?a)?(y?b)?(z?c))
??(cond?((eq?(caar?y)?x)?(cadar?y))
????????('t?(assoc.?x?(cdr?y)))))
>?(assoc.?'x?'((x?a)?(y?b)))
a
>?(assoc.?'x?'((x?new)?(x?a)?(y?b)))
new
一個驚喜
因此我們能夠定義函數來連接表,替換表達式等等.也許算是一個優美的表示法, 那下一步呢? 現在驚喜來了. 我們可以寫一個函數作為我們語言的解釋器:此函數取任意Lisp表達式作自變量并返回它的值. 如下所示: (defun?eval.?(e?a)??(cond?
????((atom?e)?(assoc.?e?a))
????((atom?(car?e))
?????(cond?
???????((eq?(car?e)?'quote)?(cadr?e))
???????((eq?(car?e)?'atom)??(atom???(eval.?(cadr?e)?a)))
???????((eq?(car?e)?'eq)????(eq?????(eval.?(cadr?e)?a)
????????????????????????????????????(eval.?(caddr?e)?a)))
???????((eq?(car?e)?'car)???(car????(eval.?(cadr?e)?a)))
???????((eq?(car?e)?'cdr)???(cdr????(eval.?(cadr?e)?a)))
???????((eq?(car?e)?'cons)??(cons???(eval.?(cadr?e)?a)
????????????????????????????????????(eval.?(caddr?e)?a)))
???????((eq?(car?e)?'cond)??(evcon.?(cdr?e)?a))
???????('t?(eval.?(cons?(assoc.?(car?e)?a)
????????????????????????(cdr?e))
??????????????????a))))
????((eq?(caar?e)?'label)
?????(eval.?(cons?(caddar?e)?(cdr?e))
????????????(cons?(list?(cadar?e)?(car?e))?a)))
????((eq?(caar?e)?'lambda)
?????(eval.?(caddar?e)
????????????(append.?(pair.?(cadar?e)?(evlis.?(cdr??e)?a))
?????????????????????a)))))
(defun?evcon.?(c?a)
??(cond?((eval.?(caar?c)?a)
?????????(eval.?(cadar?c)?a))
????????('t?(evcon.?(cdr?c)?a))))
(defun?evlis.?(m?a)
??(cond?((null.?m)?'())
????????('t?(cons?(eval.??(car?m)?a)
??????????????????(evlis.?(cdr?m)?a))))) eval.的定義比我們以前看到的都要長. 讓我們考慮它的每一部分是如何工作的.
eval.有兩個自變量: e是要求值的表達式, a是由一些賦給原子的值構成的表,這些值有點象函數調用中的參數. 這個形如pair.的返回值的表叫做環境. 正是為了構造和搜索這種表我們才寫了pair.和assoc..
eval.的骨架是一個有四個子句的cond表達式. 如何對表達式求值取決于它的類型. 第一個子句處理原子. 如果e是原子, 我們在環境中尋找它的值:
>?(eval.?'x?'((x?a)?(y?b)))a
第二個子句是另一個cond, 它處理形如(a ...)的表達式, 其中a是原子. 這包括所有的原始操作符, 每個對應一條子句.
>?(eval.?'(eq?'a?'a)?'())t
>?(eval.?'(cons?x?'(b?c))
?????????'((x?a)?(y?b)))
(a?b?c) 這幾個子句(除了quote)都調用eval.來尋找自變量的值.
最后兩個子句更復雜些. 為了求cond表達式的值我們調用了一個叫 evcon.的輔助函數. 它遞歸地對cond子句進行求值,尋找第一個元素返回t的子句. 如果找到了這樣的子句, 它返回此子句的第二個元素.
>?(eval.?'(cond?((atom?x)?'atom)????????????????('t?'list))
?????????'((x?'(a?b))))
list
第二個子句的最后部分處理函數調用. 它把原子替換為它的值(應該是lambda 或label表達式)然后對所得結果表達式求值. 于是
(eval.?'(f?'(b?c))???????'((f?(lambda?(x)?(cons?'a?x))))) 變為 (eval.?'((lambda?(x)?(cons?'a?x))?'(b?c))
???????'((f?(lambda?(x)?(cons?'a?x))))) 它返回(a b c).
eval.的最后cond兩個子句處理第一個元素是lambda或label的函數調用.為了對label 表達式求值, 先把函數名和函數本身壓入環境, 然后調用eval.對一個內部有 lambda的表達式求值. 即:
(eval.?'((label?firstatom?(lambda?(x)????????????????????????????(cond?((atom?x)?x)
??????????????????????????????????('t?(firstatom?(car?x))))))
?????????y)
???????'((y?((a?b)?(c?d))))) 變為 (eval.?'((lambda?(x)
???????????(cond?((atom?x)?x)
?????????????????('t?(firstatom?(car?x)))))
?????????y)
????????'((firstatom
???????????(label?firstatom?(lambda?(x)
????????????????????????????(cond?((atom?x)?x)
??????????????????????????????????('t?(firstatom?(car?x)))))))
??????????(y?((a?b)?(c?d))))) 最終返回a.
最后,對形如((lambda (p1) e) a1...an)的表達式求值,先調用evlis.來求得自變量(a1...an)對應的值(v1...vn),把(p1v1)...(pnvn)添加到環境里, 然后對e求值. 于是
(eval.?'((lambda?(x?y)?(cons?x?(cdr?y)))?????????'a
?????????'(b?c?d))
???????'()) 變為 (eval.?'(cons?x?(cdr?y))
???????'((x?a)?(y?(b?c?d)))) 最終返回(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.來代替. 即
(assoc. (car e) a) 能寫成 (eval.?'((label?assoc.????????????????(lambda?(x?y)
??????????????????(cond?((eq?(caar?y)?x)?(cadar?y))
????????????????????????('t?(assoc.?x?(cdr?y))))))
?????????(car?e)
?????????a)
????????(cons?(list?'e?e)?(cons?(list?'a?a)?a)))
麥卡錫的eval有一個錯誤. 第16行是(相當于)(evlis. (cdr e) a)而不是(cdr e), 這使得自變量在一個有名函數的調用中被求值兩次. 這顯示當論文發表的時候, eval的這種描述還沒有用IBM 704機器語言實現. 它還證明了如果不去運行程序, 要保證不管多短的程序的正確性是多么困難.
我還在麥卡錫的論文中碰到一個問題. 在定義了eval之后, 他繼續給出了一些更高級的函數--接受其它函數作為自變量的函數. 他定義了maplist:
(label?maplist???????(lambda?(x?f)
?????????(cond?((null?x)?'())
???????????????('t?(cons?(f?x)?(maplist?(cdr?x)?f)))))) 然后用它寫了一個做微分的簡單函數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
?
轉載于:https://www.cnblogs.com/Henrya2/archive/2009/02/07/1386010.html
總結
- 上一篇: .NET设计模式(15):结构型模式专题
- 下一篇: [转]如何讓IE7中關閉瀏覽器不出現詢問