歡迎來到Lisp的世界
本章的目標是盡快讓你編程. 在本章結束的時候,你會掌握足夠的Common Lisp的知識,可以開始寫程序了.范式(form)
你可以通過使用Lisp而學習它,這是千真萬確的,因為Lisp是交互式語言. 任何 Lisp系統都包含一個叫做頂層(toplevel)的交互式前端. 你在頂層中輸入Lisp表達式,系統打印它們的值. Lisp通常打印一個提示符表示它正在等待你的輸入. 許多Common Lisp的實現用
> 1 1 >系統會打印它的值,跟著另一個提示符,表示它在等待更多的輸入. 在這種情況下,打印出來的值和我們輸入的一樣. 象1這樣的數叫做自身求值的. 當我們輸入一個需要做些求值工作的表達式時,事情變得有趣起來. 例如,如果想把兩個數加起來,我們輸入:
> (+ 2 3) 5在表達式(+ 2 3)中,+叫做操作符,數2和3叫做變元. 在日常生活中我們會把此表達式寫為2 + 3,但在Lisp中我們把+寫在最前面,后面跟著變元,整個表達式被一對括號圍住:(+ 2 3). 因為操作符在前,這叫做前綴表示法. 一開始這樣寫表達式有點怪,但事實上這種表示法是 Lisp最好的東西之一. 比如,我們想把三個數加起來,用通常的表示法我們要寫+兩次:
2 + 3 + 4而在Lisp中我們僅需增加一個變元:
> (+ 2 3 4)通常我們用+,它必須有兩個變元:一個在左邊,一個在右邊. 前綴表示法的彈性意味著,在Lisp中,+可以接受任意數目的變元,包括零個:
> (+) 0 > (+ 2) 2 > (+ 2 3) 5 > (+ 2 3 4) 9 > (+ 2 3 4 5) 14因為操作符可以接受不同數目的變元,我們需要用括號指示表達式的開始和結束. 表達式可以嵌套. 即表達式中的變元本身可能是個復雜的表達式:
> (/ (- 7 1) (- 4 2)) 3用自然語言來說,七減一的結果被四減二的結果除. 另一個Lisp表示法的漂亮之處是:它無所不包. 所有的Lisp表達式要么是象1這樣原子(atom),要么是放在括號中由零個或多個表達式組成的表(list). 這些是合法的Lisp表達式:
2 (+ 2 3) (+ 2 3 4) (/ (- 7 1) (- 4 2))正如我們將要看到的,所有的Lisp代碼都采取這種形式. 象C這樣的語言有著更復雜的語法:算術表達式用中綴表示法;函數調用類似前綴表示法,自變量用逗號隔開;表達式用分號隔開;而代碼塊用花括號分隔. 在Lisp中我們用單一的記號表達所有這些概念.
求值
在上一節中,我們在頂層里輸入表達式,Lisp顯示它們的值. 在這節里我們仔細觀察一下表達式是如何求值的. 在Lisp中,+是一個函數,形如(+ 2 3)的表達式是函數調用. 當Lisp對函數調用求值時,它做這樣兩步:- 首先變元從左至右被求值. 在此例中,每個變元求值到自身,所以變元的值分別是2和3.
- 變元的值傳給以操作符命名的函數. 在此例中,即+函數,它返回5.
- Lisp計算(- 7 1): 7求值為7,1求值為1. 它們被傳給函數-,它返回6.
- Lisp計算(- 4 2): 4求值為4,2求值為2. 它們被傳給函數-,它返回2.
- 6和2的值傳給函數/,它返回3.
> (quote (+ 3 5)) (+ 3 5)為了方便,Common Lisp定義'作為quote的簡記法. 通過在任何表達式前面加上' 你能獲得與調用quote同樣的效果:
> '(+ 3 5) (+ 3 5)用簡記法比用quote普遍得多. Lisp提供quote作為一種保護表達式以防被求值的手段. 下一節會解釋這種保護是多么有用.
從麻煩中解脫出來 如果你輸入了一些Lisp不能理解的東西,它會打印一條出錯信息并把你帶到一個叫中斷循環(break loop)的頂層中去. 中斷循環給了有經驗的程序員弄清出錯原因的機會, 不過一開始你唯一需要知道的事是如何從中斷循環中出來. 如何返回頂層取決于你用的Lisp環境. 在這個假設的環境里,用:abort出來:
> (/ 1 0) Error: Division by zero. Options: :abort, :backtrace >> :abort >附錄A展示了如何調試Lisp程序,以及一些最常見錯誤的例子.
數據
Lisp提供所有我們能在其它語言中找得到的數據類型,和一些我們找不到的. 一種我們早已使用的數據類型是整數,它寫為一列數字:256. 另一種和其它語言一樣有的是字符串,它表示為一列用雙引號括起來的字符:"ora et labora". 整數和字符串都求值到自身. 另兩種我們不常在其它語言中發現的是符號和表. 符號是單詞. 通常它們被轉換成大寫,不管你如何輸入:> 'Artichoke ARTICHOKE符號(通常)不求值為自身,因此如果你想引用一個符號,請象上面那樣用'引用它. 表表示為被括號包圍的零個或多個元素. 元素可以是任何類型,包括表. 你必須引用表,否則Lisp會以為它是函數調用:
> '(my 3 "Sons") (MY 3 "Sons") > '(the list (a b c) has 3 elements) (THE LIST (A B C) HAS 3 ELEMENTS)請注意一個引號保護整個表達式,包括里面的表達式. 你可以調用list來構造表. 因為list是一個函數,它的變元被求值. 這是+調用在 list調用里的例子:
> (list 'my (+ 2 1) "Sons") (MY 3 "Sons")現在我們處于欣賞Lisp最非同尋常特征之一的位置上. Lisp程序表達為表. 如果變元的機動性和優雅性沒能說服你Lisp記號是一種有價值的工具,這點應該能使你信服. 這意味著Lisp程序可以生成Lisp代碼. Lisp程序員能(而且經常)為自己編寫能寫程序的程序. 我們到第10章才考慮這種程序,但即使在現階段理解表達式和表的關系也是很重要的,而不是被它們弄糊涂. 這就是為何我們使用quote. 如果一個表被引用了, 求值返回它本身; 如果沒有被引用,它被認為是代碼,求值返回它的值:
> (list '(+ 2 1) (+ 2 1)) ((+ 2 1) 3)此處第一個變元被引用了,所以生成了一個表. 第二個變元沒有被引用,視之為函數調用,得出一個數字. 在Common Lisp中有兩種方法表示空表. 你可用一對不包含任何東西的括號來表示空表,或用符號nil來表示它. 你用哪種方法表示空表都沒有關系,不過它會被顯示成nil:
> () NIL > nil NIL你不必引用nil(雖然這也沒什么害處)因為nil求值到自身.
表的操作
函數cons構造表. 如果它的第二個變元是表,它返回一個新表,新表的第一個元素就是第一個變元:> (cons 'a '(b c d)) (A B C D)我們可以通過把新元素cons到空表來構造新表. 我們在上一節見到的list函數只不過是一個把幾樣東西cons到nil上去的方便辦法:
> (cons 'a (cons 'b nil)) (A B) > (list 'a 'b) (A B)基本的提取表中元素的函數是car和cdr.1 表的car就是它的第一個元素,而 cdr是第一個元素后面的所有東西:
> (car '(a b c)) A > (cdr '(a b c)) (B C)你能用car和cdr的組合來取得表中任何元素. 如果你想取第三個元素,可以這樣:
> (car (cdr (cdr '(a b c d)))) C但是,你可以用third更容易地做同樣的事:
> (third '(a b c d)) C
真值
符號t是Common Lisp中表示真的缺省值. 就象nil,t求值到自身. 函數listp返回真如果它的變元是一個表:> (listp '(a b c)) T一個函數叫做斷言如果它的返回值被解釋成真或假. Common Lisp的斷言的名字通常以p結尾. 假在Common Lisp中用nil(空表)來表示. 如果我們傳給listp的變元不是表,它返回nil:
> (listp 27) NIL因為nil扮演兩個角色,函數null返回真如果它的變元是空表:
> (null nil) T而函數not返回真如果它的變元是假:
> (not nil) T它們完全做的是同樣的事情. 要if是Common Lisp中最簡單的條件語句. 它一般接受三個變元:一個測試表達式, 一個then表達式和一個else表達式. 測試表達式被求值. 如果它返回真,則then 表達式被求值并返回結果. 如果它返回假,則else表達式被求值并返回它的結果:
> (if (listp '(a b c)) (+ 1 2) (+ 5 6)) 3 > (if (listp 27) (+ 1 2) (+ 5 6)) 11就象quote,if是特殊操作符. 它不能用函數來實現,因為函數調用的變元總是要求值的,而if的特點是只有最后兩個變元中的一個被求值. if的最后一個變元是可選的. 如果你省略它,它缺省為nil:
> (if (listp 27) (+ 2 3)) NIL雖然t是真的缺省表示,任何不是nil的東西在邏輯上下文中被認為是真:
> (if 27 1 2) 1邏輯操作符and和or就象條件語句. 兩者都接受任意數目的變元,但只求值能夠確定返回值的數目的變元. 如果所有的變元都是真(不是nil),那么and返回最后變元的值:
> (and t (+ 1 2)) 3但如果其中一個變元是假,所有它后面的變元都不求值了. or也類似,只要它碰到一個是真的變元就繼續求值了. 這兩個操作符是宏. 就象特殊操作符,宏可以規避通常的求值規則. 第10章解釋如何編寫你自己的宏.
函數
你可以用defun來定義新的函數. 它通常接受三個以上變元:一個名字,一列參數, 和組成函數體的一個或多個表達式. 這是我們定義third的一種可能:> (defun our-third (x) (car (cdr (cdr x)))) OUR-THIRD第一個變元表示函數名將是our-third. 第二個變元,表(x),說明函數將接受一個變元:x. 象這樣用作占位符的符號叫做變量. 當變量代表傳給函數的變元, 就象x所做的,它又叫做參數. 定義的其余部分,(car (cdr (cdr x))),即通常所說的函數體. 它告訴Lisp,為了計算函數的返回值,它該做些什么. 所以,對我們給出的作為變元的任何x,調用 our-third會返回(car (cdr (cdr x))):
> (our-third '(a b c d)) C既然我們看到了變量,就更容易理解什么是符號了. 他們是變量名,是一種有自己名稱的對象. 這就是為什么符號要象表一樣必須被引用. 表必須被引用是因為不如此的話,它就會被當作代碼;符號必須被引用是因為不如此的話,它就會被當作變量. 你可以把函數定義想象成某個Lisp表達式的一般形式. 下面的表達式測試1和4之和是否大于3:
> (> (+ 1 4) 3) T通過把這些特殊數字換成變量,我們可以寫一個函數測試任何兩個數之和是否大于第三個:
> (defun sum-greater (x y z) (> (+ x y) z)) SUM-GREATER > (sum-greater 1 4 3) TLisp對程序,過程或函數不加區別. 函數做了所有的事情(事實上構成了語言本身的大部分). 你可以認為你的函數中的一個是主函數,但通常你能在頂層里調用任何一個函數. 這意味著,當你寫程序的時候,你能一小段一小段地測試它們.
遞歸
我們在上一節中定義的函數還調用了其它函數為自己服務. 比如sum-greater調用了+和>. 函數可以調用任何函數,包括它本身. 自己調用自己的函數是遞歸的. Common Lisp函數member測試某樣東西是否是一個表的元素. 這是定義成遞歸函數的簡化版本:(defun our-member (obj lst) (if (null lst) nil (if (eql (car lst) obj) lst (our-member obj (cdr lst)))))斷言eql測試它的兩個變元是否相同;除此之外,定義中所有東西我們以前都見過. 這是它的運行情況:
> (our-member 'b '(a b c)) (B C) > (our-member 'z '(a b c)) NILour-member的定義符合下面的自然語言描述. 為了測試一個對象obj是否是表lst 的成員,我們
- 首先檢查lst是否為空. 如果是空的,顯然obj不是它的成員,我們做完了.
- 否則如果obj是lst的第一個元素,它就是成員
- 否則只有當obj是lst其余部分的成員時,它才是lst的成員.
- 取得一份文獻
- 查找有關人口變化的信息
- 如果該文獻提到其它可能有用的文獻,檢查它們
閱讀Lisp
上一節我們定義的our-member以五個括號結尾. 更加復雜的函數定義可能以七八個括號結尾. 初學Lisp的人看到這么多括號會感到氣餒. 這叫人如何去讀這樣的代碼? 更不用說寫了. 我怎么分辨括號之間的匹配? 回答是,你不需要做這些. Lisp程序員通過縮進,而不是括號來讀寫程序. 當他們寫代碼的時候,他們讓文本編輯器顯示哪個括號匹配哪個. 任何一個優秀的編輯器,特別是Lisp系統附帶的,應該能做到括號匹配. 在這樣的編輯器中,當你輸入了一個括號,它會指示和它匹配的那個括號. 如果你的編輯器不做括號匹配,現在就停下來,研究一個如何使它做這件事,因為沒有這個功能,事實上不可能寫Lisp 程序的. 在vi中,你可以用:set sm來打開括號匹配. 在emacs中,M-x lisp-mode是獲得該功能的好辦法. 有了好的編輯器,當你寫程序的時候,括號匹配不再是個問題. 而且因為Lisp縮進有通用的慣例,你閱讀Lisp代碼也不是個問題了. 因為人人都用相同的習慣,你可以通過縮進閱讀代碼,忽略括號. 不管多么有經驗的Lisp黑客,會發現our-member的定義很難讀懂,如果它寫成這個樣子:(defun our-member (obj lst) (if (null lst) nil (if (eql (car lst) obj) lst (our-member obj (cdr lst)))))如果代碼適當地縮進,他就沒有困難了. 你可以忽略大部分的括號而讀懂它:
defun our-member (obj lst) if null lst nil if eql (car lst) obj lst our-member obj (cdr lst)事實上,當你在紙上寫Lisp代碼的時候,這就是一個可行的辦法. 以后你輸入的時候,可以充分利用編輯器的匹配括號的功能.
輸入和輸出
到目前為止,我們一直在利用頂層暗中使用i/o. 對實際的交互式的程序,這可能還不夠. 在這一節中,我們看一些輸入輸出函數. Common Lisp中最一般的輸出函數是format. 它接受兩個以上變元:第一個表示輸出到哪兒,第二個是字符串模板,剩下的變元通常是對象,它們的打印表示 (printed representation)將被插入到模板中去. 這是個典型的例子:> (format t "~A plus ~A equals ~A.~%" 2 3 (+ 2 3)) 2 plus 3 equals 5. NIL注意兩樣東西打印在這兒. 第一行是format打印的. 第二行是format調用的返回值,就象通常一樣由頂層打印. 通常象format這樣的函數不會直接在頂層,而是在程序內部被調用,因此返回值就不會被看見. format的第一個變元t表示輸出將被送到缺省的地方去. 通常這會是頂層. 第二個變元是充當輸出模板的字符串. 在它里面,每個 A*表示一個將被填充的位置, 而 %表示新行符. 這些位置依次被后面的變元的值填充. 標準的輸入函數是read. 當沒有變元時,它從缺省的地方--通常是頂層--讀入. 下面這個函數提示用戶輸入,然后返回任何輸入的東西:
(defun askem (string) (format t "~A" string) (read))它運行如下:
> (askem "How old are you? ") How old are you? 29 29請記住read會永遠等在那兒直到你輸入什么東西并(通常要)敲入回車. 因此調用 read而不打印明確的提示信息是不明智的,否則你的程序會給人以已經死掉的印象,但實際上它在等待輸入. 第二個要了解read的是它非常強大:它是一個完整的Lisp語法分析器. 它并不是讀入字符再把它們當作字符串返回. 它分析所讀到的東西,并返回所產生的Lisp 對象. 在上例中, 它返回一個數. askem雖然很短,但它展示了一些我們以前在函數定義中沒有看到的內容. 它的函數體包含多個表達式. 函數體可以包含任意多個表達式,當函數被調用時,它們依次被求值,函數會返回最后一個表達式的值. 在以前的章節中,我們堅持所謂的``純粹''的Lisp--即沒有副作用的Lisp. 副作用是指作為表達式求值的后果改變了外部世界的狀態. 當我們對一個純粹的Lisp 表達式,例如(+ 1 2)求值,沒有出現副作用;它僅返回一個值. 但當我們調用 format,它不僅返回值,還打印了一些東西. 這是一種副作用. 如果我們要寫沒有副作用的代碼,那么定義有多個表達式的函數體就沒有什么意義. 最后一個表達式的值作為函數的返回值被返回了,但前面的表達式的值都被扔掉了. 如果這些表達式沒有副作用,你就不知道為什么Lisp要費勁去計算它們.
變量
let是Common Lisp里最常用的操作符之一,它讓你引入新的局部變量:> (let ((x 1) (y 2)) (+ x y)) 3一個let表達式有兩部分. 第一部分是一列創造新變量的指令,每個形如(變量 表達式). 每個變量會被賦予相應的表達式的值. 在上例中,我們創造了兩個變量x 和y,它們分別被賦予初值1和2. 這些變量只在let的體內有效. 變量和值的列表的后面是一組表達式,它們將被依次求值. 在此例中,只有一個表達式:對+的調用. 最后一個表達式的值作為let的值被返回. 下面是一個使用let 的更具選擇性的askem的版本:
(defun ask-number () (format t "Please enter a number. ") (let ((val (read))) (if (numberp val) val (ask-number))))此函數造了變量val來存放read返回的對象. 因為它有此對象的名稱,它可以在作出是否要返回對象之前察看一下你的輸入值. 你可能已經猜到,numberp是測試它的自變量是否是數字的斷言. 如果用戶輸入的不是數字,ask-number調用它自己. 結果產生了一個堅持要得到一個數的函數:
> (ask-number) Please enter a number. a Please enter a number. (ho hum) Please enter a number. 52 52象目前我們看到的變量都叫做局部變量. 它們只在特定的環境中是有效的. 另外有一類叫做全局變量的變量,它們在任何地方都是可見的.3 通過傳給defparameter一個符號和一個值,你可以構造全局變量:
> (defparameter *glob* 99) *GLOB*這樣的變量可以在任何地方存取,除非在一個表達式中,定義了一個相同名字的局部變量. 為了避免這種情況的出現,習慣上全局變量的名字以星號開始和結束. 我們剛才定義的變量可讀作``星-glob-星''. 你還可以用defconstant定義全局常數:
(defconstant limit (+ *glob* 1))你不需要給常數起一個與眾不同的名字,因為如果使用相同的名字作為變量,就會出錯. 如果你想知道某個符號是否是全局變量或常數的名字,請用boundp:
> (boundp '*glob*) T
賦值
Common Lisp中最普通的賦值操作符是setf. 我們可以用它對全局或局部變量進行賦值:> (setf *glob* 98) 98 > (let ((n 10)) (setf n 2) n) 2如果第一個自變量不是局部變量的名字,它被認為是全局變量:
> (setf x (list 'a 'b 'c)) (A B C)即你可以通過賦值隱含地新建全局變量.不過在源文件中明確地使用 defparameter
是較好的風格. 你能做的遠不止給變量賦值. setf的第一個自變量不但可以是變量名,還可以是表達式. 在這種情況下,第二個自變量的值被插入到第一個所涉及到的位置:
> (setf (car x) 'n) N > x (N B C)setf的第一個自變量幾乎可以是任何涉及特定位置的表達式. 所有這樣的操作符在附錄D中都被標記為``settable''. 你可以給setf偶數個自變量. 形如
(setf a b c d e f)的表達式相當于連續三個單獨的setf調用:
(setf a b) (setf c d) (setf e f)
函數化編程法
函數化編程法的意思是編寫通過返回值來工作的程序,而不是修改什么東西. 它是 Lisp中占支配地位的范例. 大多數Lisp內置函數被調用是為了得到它們的返回值, 而不是它們的副作用. 例如函數remove,它接受一個對象和一個表,返回一個排除了那個對象的新表:> (setf lst '(c a r a t)) (C A R A T) > (remove 'a lst) (C R T)為什么不說remove從表中刪除一個對象? 因為這不是它所做的事情. 原來的表沒有被改變:
> lst (C A R A T)那么如果你真想從表中刪掉一些元素怎么辦? 在Lisp中,你通常這樣做類似的事情:把表傳給某個函數,然后用setf來處理返回值. 為了把所有的a從表x中刪掉, 我們這樣做:
(setf x (remove 'a x))函數化編程法本質上意味著避免使用諸如setf的函數. 乍一看連想象這種可能性都很因難,別說試著去做了. 怎么能僅憑返回值就能構造程序? 完全不利用副作用是有困難的. 但隨著學習的深入,你會驚訝地發現真正需要副作用的地方極少. 你使用副作用越少,你也就越進步. 函數化編程最重要的優點之一是它允許交互式測試. 在純粹的函數化代碼中,當你寫函數的時候就可以測試它們. 如果它返回期望的值,你可以肯定它是正確的. 這些額外的信心,聚集在一起會產生巨大的影響. 當你在程序中修改了任何地方, 你會得到即時的轉變. 而這種即時的轉變會帶來一種全新的編程風格. 就象電話與信件相比,賦予我們新的通訊方式.
迭代
當我們想做一些重復的事情時,用迭代比用遞歸更自然些. 典型的例子是用迭代生成某種表格. 函數(defun show-squares (start end) (do ((i start (+ i 1))) ((> i end) 'done) (format t "~A ~A~%" i (* i i))))打印從start到end之間的整數的平方:
> (show-squares 2 5) 2 4 3 9 4 16 5 25 DONEdo宏是Common Lisp中最基本的迭代操作符. 就象let,do也會產生變量,它的第一個自變量是關于變量規格的表. 表中的每個元素具有如下形式:
(variable initial update)其中variable是符號,而initial和update是表達式. 一開始每個變量會被賦予相應的initial的值;在迭代的時候它會被賦予相應的update的值. show-squares中的do僅產生了一個變量i. 第一次迭代的時候,i被賦予start的值,在以后的迭代中它的值會每次增加1. do的第二個自變量是包含一個或多個表達式的表. 第一個表達式用來測試迭代是否應該停止. 在上例中,測試是(> i end). 其余的表達式會在迭代停止后依次計算,并且最后一個的值作為do的返回值. 因此show-squares總是會返回done. 余下的自變量組成了循環體. 它們在每次迭代的時候依次被求值. 在每次迭代的時候變量先被更新,然后終止測試被計算,再是(如果測試失敗)循環體被計算. 作為對比,這是遞歸版本的show-squares:
(defun show-squares (i end) (if (> i end) 'done (progn (format t "~A ~A~%" i (* i i)) (show-squares (+ i 1) end))))此函數中的唯一新面孔是progn. 它接受任意數量的表達式,對它們依次求值,然后返回最后一個的值. 對一些特殊情況Common Lisp有更簡單的迭代操作符. 比如,為了遍歷表的所有元素,你更可能用dolist. 這個函數返回表的長度:
(defun our-length (lst) (let ((len 0)) (dolist (obj lst) (setf len (+ len 1))) len))此處dolist接受形如(variable expression)的自變量,然后是表達式塊. variable相繼與expression返回的表中元素綁定,表達式塊被計算. 因此上面的循環在意思是,對lst中的每個obj,len增加1. 此函數的一個顯然的遞歸版本是:
(defun our-length (lst) (if (null lst) 0 (+ (our-length (cdr lst)) 1)))即,如果表為空,它的長度就是0;否則它的長度是它的cdr的長度加上1. 此版本清楚一些,但因為它不是尾遞歸的(見13.2節),它的效率不那么高.
函數作為對象
函數在Lisp中就象符號,字符串和表一樣,是常規的對象. 如果我們給function一個函數的名字,它會返回相關的對象. 就象quote,function是特殊操作符,因此我們不必引用自變量:> (function +) #<Compiled-Function + 17BA4E>這個模樣很奇怪的返回值是函數在典型Common Lisp實現中可能的顯示方式. 到目前為止我們涉及到的對象具有這樣的特點:Lisp顯示它們與我們輸入的模樣是一致的. 此慣例不適合函數. 一個象+這樣的內置函數在內部可能是一段機器碼. Common Lisp的實現可以選擇任何它喜歡的外部表示方式. 就象我們用'作為quote的簡記法,我們可以用#'作為function的簡寫:
> #'+ #<Compiled-Function + 17BA4E>此簡記法叫做sharp-quote. 就象其它的對象,函數可以作為自變量傳遞. 一個接受函數作為自變量的是 apply. 它接受一個函數和一列自變量,并返回那個函數應用這些自變量后的結果:
> (apply #'+ '(1 2 3)) 6 > (+ 1 2 3) 6它能接受任意數目的自變量,只要最后一個是表:
> (apply #'+ 1 2 '(3 4 5)) 15函數funcall能做同樣的事情,不過它不需要把自變量放在表中:
> (funcall #'+ 1 2 3) 6宏defun創造一個函數并給它一個名字. 但函數并不是必須需要名字,因此我們也不需要用defun來定義它們. 就象其它Lisp對象一樣,我們可以直接引用函數. 為了直接引用一個整數,我們用一列數字;為了直接引用函數,我們用所謂的 lambda表達式. 一個lambda表達式是包含以下元素的表:符號lambda,一列參數, 然后是零個或多個表達式組成的函數體. 這個lambda表達式代表接受兩個數并返回它們之和的函數:
(lambda (x y) (+ x y))(x y)是參數表,跟在它后面的是函數體. 可以認為lambda表達式是函數的名稱. 就象普通的函數名,lambda表達式可以是函數調用的第一個元素:
> ((lambda (x) (+ x 100)) 1) 101而通過在lamda表達式之前附加#',我們得到了相應的函數:
> (funcall #'(lambda (x) (+ x 100)) 1) 101此種表達法讓我們使用匿名函數.
lambda是什么? lambda表達式中的lambda不是操作符. 它僅是個符號.4 它在早期的Lisp方言里有一種作用:函 數的內部形態是表,因此區別函數和普通表的唯一辦法是查看第一個元素是否是 符號lambda. 在Common Lisp中你能把函數表示為表,但它們在內部被表示成獨特的函數對象. 因此lambda不再是必需的. 如果要求把函數
(lambda (x) (+ x 100))表示成
((x) (+ x 100))也沒有什么矛盾,但Lisp程序員已經習慣了函數以符號lambda開始,因此Common Lisp保留了此傳統.
類型
Lisp用非同尋常的靈活手段來處理類型. 在許多語言中,變量是有類型的,你得指定變量的類型才能使用它. 在Common Lisp中,值具有類型,而不是變量. 你可以假想每個對象都貼了指明它的類型的標簽. 這種方法叫做顯示類型. 你不需要去聲明變量的類型,因為變量可以裝任何類型的對象. 雖然類型聲明不是必需的,為了效率的緣故你可能會用到它們. 類型聲明在13.3 節中討論. Common Lisp的內置類型構成了一個類型的層次結構. 一個對象通常具有多種類型. 比如,數27是類型fixnum,integer,rational,real,number,atom,和t,以一般性的增長為序. (Numeric類型在第9章中討論)類型t是所有類型的超集,因此任何對象都是類型t. 函數typep接受一個對象和一個類型說明符,如果對象是那種類型就返回真:> (typep 27 'integer) T當我們碰到各種內置類型時,我們會介紹它們.
展望
本章僅蜻蜓點水般地介紹了一下Lisp. 然而一種非同尋常的語言的形象已經浮現出來了. 首先,該語言有單一的語法來表達所有的程序結構. 此語法基于一種Lisp 的對象--表. 函數,作為獨特的Lisp對象,可以表示為表. 而且Lisp本身就是 Lisp程序,幾乎全是由與你自己定義的函數沒有任何區別的函數組成的. 如果你還不完全清楚所有這些概念之間的關系,請不必擔心. Lisp引入了這么多新穎的概念,你得花時間去熟悉它們. 不過至少得說明一件事: 令人吃驚的優雅思想蘊藏其中. Richard Gabriel曾半開玩笑地說C是適合寫Unix的語言.5 我們也可以說Lisp是編寫Lisp的語言. 但這是兩種不同的陳述. 一種可以用自己來編寫的語言是和一種擅長編寫某些特定類型的應用的語言完全不同的. 它開啟了新的編程方法:你不但在語言中編程,你還可以改進語言以適合你程序的需要. 如果你想理解Lisp編程的本質,這個思想是個很好的起點.總結
- Lisp是交互式語言. 如果你在頂層輸入表達式,Lisp會打印它的值.
- Lisp程序由表達式組成. 表達式可以是一個原子,或是一個表, 表的第一個元素是操作符,后面跟著零個或多個自變量. 前綴表達式意味著操作符可接受任意多個自變量.
- Common Lisp函數調用的求值規則:從左至右對自變量求值,然后把這些值傳給由操作符表示的函數. quote有它自己的求值規則:它原封不動地返回自變量.
- 除了通常的數據類型,Lisp還有符號和表. 由于Lisp程序由表組成,很容易編寫能寫程序的程序.
- 三個基本的表處理函數是cons:它創造一個表;car:它返回表的頭一個元素; cdr:它返回第一個元素之后的所有東西.
- 在Common Lisp里, t表示真,nil表示偽. 在邏輯上下文中,除了nil之外的任何東西都算作真. 基本的條件語句是if. and和or操作符就象條件語句.
- Lisp主要是由函數構成的. 你可用defun來定義新的函數.
- 調用自己的函數是遞歸的. 遞歸函數應該被認為是一個過程而不是機器.
- 括號不是個問題,因為程序員利用縮進來讀寫Lisp.
- 基本的i/o函數是read:它包含了完整的Lisp語法分析器,和format:它基于模板產生輸出.
- 你可以用let創造新的局部變量,用defparameter創造新的全局變量.
- 賦值操作符是setf. 它的第一個自變量可以是表達式.
- 函數化編程法--它意味著避免副作用--是Lisp中占支配地位的范例.
- 基本的循環操作符是do.
- 函數是常規的Lisp對象. 它們可以作為自變量被傳遞,可以表示成lambda 表達式.
- 值有類型,而變量沒有類型
練習
- 解釋以下表達式求值后的結果:
- a. (+ (- 5 1) (+ 3 7))
- b. (list 1 (+ 2 3))
- 給出3種不同的能返回(a b c)的cons表達式
- 用car和cdr定義一個函數,它返回表的第四個元素.
- 定義一個函數,它接受兩個自變量,返回兩個中較大的一個.
- 這些函數做了什么?
a. (defun enigma (x) (and (not (null x)) (or (null (car x)) (enigma (cdr x))))) b. (defun mystery (x y) (if (null y) nil (if (eql (car y) x) 0 (let ((z (mystery x (cdr y)))) (and z (+ z 1))))))
- 在下面的表達式中,x處應該是什么可得出結果?
a. > (car (x (cdr '(a (b c) d)))) B b. > (x 13 (/ 1 0)) 13 c. > (x #'list 1 nil) (1)
- 只用本章介紹的操作符,定義一個函數,它接受一個表作為自變量,并返回t 如果表的元素中至少有一個類型是表.
- 給出函數的迭代和遞歸版本:它
a. 接受一個正整數,并打印這么多數目的點. b. 接受一個表,返回符號a在表中出現的次數.
- 一位朋友想寫一個函數,它返回表中所有非nil元素之和. 他寫了此函數的兩個版本, 但沒有一個能正確工作. 請指出錯誤在哪里,并給出正確的版本:
a. (defun summit (lst) (remove nil lst) (apply #'+ lst)) b. (defun summit (lst) (let ((x (car lst))) (if (null x) (summit (cdr lst)) (+ x (summit (cdr lst))))))
About this document ...
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 acl2.tex
The translation was initiated by Dai Yuwen on 2003-07-29
Footnotes
- ... 基本的提取表中元素的函數是car和cdr.1
- car和cdr的名字來源于表在第一個Lisp實現中的內部表示. car表示``contents of the address part of the registe''而cdr表示``contents of the decrement part of the register.''
- ... 不存在了.2
- 理解遞歸有困難的讀者可以參考以下文獻中的任何一種: Touretzky, David S. Common Lisp: A Gentle Introduction to Symbolic Computation. Benjamin/Cummings, Redwood City (CA), 1990, Chapter 8. Friedman, Daniel P., and Matthias Felleisen. The Little Lisper. MIT Press, Cambridge, 1987.
- ... 有一類叫做全局變量的變量,它們在任何地方都是可見的.3
- 真正的區別在于詞法變量和特殊變量的不同,不過我們得到第六章才會考慮它.
- ... 它僅是個符號.4
- 在Ansi Common Lisp中還有一個lambda宏,它能讓你把#'(lambda (x) x)寫成(lambda (x) x). 由于使用這個宏模糊了lambda表達式和符號化的函數名(其中你得作用#') 的對稱性,它最多不過具有美觀的外表.
- ... Gabriel曾半開玩笑地說C是適合寫Unix的語言.5
- Gabriel, Richard P. Lisp: Good News, Bad News, How to Win Big. AI Expert, June 1991, p. 34.
Dai Yuwen 2003-07-29