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