本章的目的是讓你盡快開始編程。本章結(jié)束時,你會掌握足夠多的 Common Lisp 知識來開始寫程序。
人可以通過實踐來學(xué)習(xí)一件事,這對于 Lisp 來說特別有效,因為 Lisp 是一門交互式的語言。任何 Lisp 系統(tǒng)都含有一個交互式的前端,叫做頂層(toplevel)。你在頂層輸入 Lisp 表達(dá)式,而系統(tǒng)會顯示它們的值。
Lisp 通常會打印一個提示符告訴你,它正在等待你的輸入。許多 Common Lisp 的實現(xiàn)用?>
?作為頂層提示符。本書也沿用這個符號。
一個最簡單的 Lisp 表達(dá)式是整數(shù)。如果我們在提示符后面輸入?1
?,
> 1
1
>
系統(tǒng)會打印出它的值,接著打印出另一個提示符,告訴你它在等待更多的輸入。
在這個情況里,打印的值與輸入的值相同。數(shù)字?1
?稱之為對自身求值。當(dāng)我們輸入需要做某些計算來求值的表達(dá)式時,生活變得更加有趣了。舉例來說,如果我們想把兩個數(shù)相加,我們輸入像是:
> (+ 2 3)
5
在表達(dá)式?(+ 2 3)
?里,?+
?稱為操作符,而數(shù)字?2
?跟?3
?稱為實參。
在日常生活中,我們會把表達(dá)式寫作?2?+?3
?,但在 Lisp 里,我們把?+
?操作符寫在前面,接著寫實參,再把整個表達(dá)式用一對括號包起來:?(+?2?3)
?。這稱為前序表達(dá)式。一開始可能覺得這樣寫表達(dá)式有點怪,但事實上這種表示法是 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
由于操作符可接受不定數(shù)量的實參,我們需要用括號來標(biāo)明表達(dá)式的開始與結(jié)束。
表達(dá)式可以嵌套。即表達(dá)式里的實參,可以是另一個復(fù)雜的表達(dá)式:
> (/ (- 7 1) (- 4 2))
3
上面的表達(dá)式用中文來說是, (七減一) 除以 (四減二) 。
Lisp 表示法另一個美麗的地方是:它就是如此簡單。所有的 Lisp 表達(dá)式,要么是?1
?這樣的數(shù)原子,要么是包在括號里,由零個或多個表達(dá)式所構(gòu)成的列表。以下是合法的 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é)里我們深入理解一下表達(dá)式是如何被求值的。
在 Lisp 里,?+
?是函數(shù),然而如?(+?2?3)
?的表達(dá)式,是函數(shù)調(diào)用。
當(dāng) Lisp 對函數(shù)調(diào)用求值時,它做下列兩個步驟:
- 首先從左至右對實參求值。在這個例子當(dāng)中,實參對自身求值,所以實參的值分別是?
2
?跟?3
?。- 實參的值傳入以操作符命名的函數(shù)。在這個例子當(dāng)中,將?
2
?跟?3
?傳給?+
?函數(shù),返回?5
?。
如果實參本身是函數(shù)調(diào)用的話,上述規(guī)則同樣適用。以下是當(dāng)?(/?(-?7?1)?(-?4?2))
?表達(dá)式被求值時的情形:
- Lisp 對?
(-?7?1)
?求值:?7
?求值為?7
?,?1
?求值為?1
?,它們被傳給函數(shù)?-
?,返回?6
?。- Lisp 對?
(-?4?2)
?求值:?4
?求值為?4
?,?2
?求值為?2
?,它們被傳給函數(shù)?-
?,返回?2
?。- 數(shù)值?
6
?與?2
?被傳入函數(shù)?/
?,返回?3
?。
但不是所有的 Common Lisp 操作符都是函數(shù),不過大部分是。函數(shù)調(diào)用都是這么求值。由左至右對實參求值,將它們的數(shù)值傳入函數(shù),來返回整個表達(dá)式的值。這稱為 Common Lisp 的求值規(guī)則。
逃離麻煩
如果你試著輸入 Lisp 不能理解的東西,它會打印一個錯誤訊息,接著帶你到一種叫做中斷循環(huán)(break loop)的頂層。 中斷循環(huán)給予有經(jīng)驗的程序員一個機(jī)會,來找出錯誤的原因,不過最初你只會想知道如何從中斷循環(huán)中跳出。 如何返回頂層取決于你所使用的 Common Lisp 實現(xiàn)。在這個假定的實現(xiàn)環(huán)境中,輸入?:abort
?跳出:
> (/ 1 0)
Error: Division by zero
Options: :abort, :backtrace
>> :abort
>
附錄 A 演示了如何調(diào)試 Lisp 程序,并給出一些常見的錯誤例子。
一個不遵守 Common Lisp 求值規(guī)則的操作符是?quote
?。?quote
?是一個特殊的操作符,意味著它自己有一套特別的求值規(guī)則。這個規(guī)則就是:什么也不做。?quote
?操作符接受一個實參,并完封不動地返回它。
> (quote (+ 3 5))
(+ 3 5)
為了方便起見,Common Lisp 定義?'
?作為?quote
?的縮寫。你可以在任何的表達(dá)式前,貼上一個?'
?,與調(diào)用?quote
?是同樣的效果:
> '(+ 3 5)
(+ 3 5)
使用縮寫?'
?比使用整個?quote
?表達(dá)式更常見。
Lisp 提供?quote
?作為一種保護(hù)表達(dá)式不被求值的方式。下一節(jié)將解釋為什么這種保護(hù)很有用。
Lisp 提供了所有在其他語言找的到的,以及其他語言所找不到的數(shù)據(jù)類型。一個我們已經(jīng)使用過的類型是整數(shù)(integer),整數(shù)用一系列的數(shù)字來表示,比如:?256
?。另一個 Common Lisp 與多數(shù)語言有關(guān),并很常見的數(shù)據(jù)類型是字符串(string),字符串用一系列被雙引號包住的字符串表示,比如:?"ora?et?labora"
?[3]?。整數(shù)與字符串一樣,都是對自身求值的。
| [3] | “ora et labora” 是拉丁文,意思是禱告與工作。 |
有兩個通常在別的語言所找不到的 Lisp 數(shù)據(jù)類型是符號(symbol)與列表(lists),符號是英語的單詞 (words)。無論你怎么輸入,通常會被轉(zhuǎn)換為大寫:
> 'Artichoke
ARTICHOKE
符號(通常)不對自身求值,所以要是想引用符號,應(yīng)該像上例那樣用?'
?引用它。
列表是由被括號包住的零個或多個元素來表示。元素可以是任何類型,包含列表本身。使用列表必須要引用,不然 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á)式(包含內(nèi)部的子表達(dá)式)被求值。
你可以調(diào)用?list
?來創(chuàng)建列表。由于?list
?是函數(shù),所以它的實參會被求值。這里我們看一個在函數(shù)?list
?調(diào)用里面,調(diào)用?+
?函數(shù)的例子:
> (list 'my (+ 2 1) "Sons")
(MY 3 "Sons")
我們現(xiàn)在來到領(lǐng)悟 Lisp 最卓越特性的地方之一。Lisp的程序是用列表來表示的。如果實參的優(yōu)雅與彈性不能說服你 Lisp 表示法是無價的工具,這里應(yīng)該能使你信服。這代表著 Lisp 程序可以寫出 Lisp 代碼。 Lisp 程序員可以(并且經(jīng)常)寫出能為自己寫程序的程序。
不過得到第 10 章,我們才來考慮這種程序,但現(xiàn)在了解到列表和表達(dá)式的關(guān)系是非常重要的,而不是被它們搞混。這也就是為什么我們需要?quote
?。如果一個列表被引用了,則求值規(guī)則對列表自身來求值;如果沒有被引用,則列表被視為是代碼,依求值規(guī)則對列表求值后,返回它的值。
> (list '(+ 2 1) (+ 2 1))
((+ 2 1) 3)
這里第一個實參被引用了,所以產(chǎn)生一個列表。第二個實參沒有被引用,視為函數(shù)調(diào)用,經(jīng)求值后得到一個數(shù)字。
在 Common Lisp 里有兩種方法來表示空列表。你可以用一對不包括任何東西的括號來表示,或用符號?nil
?來表示空表。你用哪種表示法來表示空表都沒關(guān)系,但它們都會被顯示為?nil
?:
> ()
NIL
> nil
NIL
你不需要引用?nil
?(但引用也無妨),因為?nil
?是對自身求值的。
用函數(shù)?cons
?來構(gòu)造列表。如果傳入的第二個實參是列表,則返回由兩個實參所構(gòu)成的新列表,新列表為第一個實參加上第二個實參:
> (cons 'a '(b c d))
(A B C D)
可以通過把新元素建立在空表之上,來構(gòu)造一個新列表。上一節(jié)所看到的函數(shù)?list
?,不過就是一個把幾個元素加到?nil
?上的快捷方式:
> (cons 'a (cons 'b nil))
(A B)
> (list 'a 'b)
(A B)
取出列表元素的基本函數(shù)是?car
?和?cdr
?。對列表取?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
在 Common Lisp 里,符號?t
?是表示邏輯?真
?的缺省值。與?nil
?相同,?t
?也是對自身求值的。如果實參是一個列表,則函數(shù)?listp
返回?真
?:
> (listp '(a b c))
T
函數(shù)的返回值將會被解釋成邏輯?真
?或邏輯?假
?時,則稱此函數(shù)為謂詞(predicate)。在 Common Lisp 里,謂詞的名字通常以?p
結(jié)尾。
邏輯?假
?在 Common Lisp 里,用?nil
?,即空表來表示。如果我們傳給?listp
?的實參不是列表,則返回?nil
?。
> (listp 27)
NIL
由于?nil
?在 Common Lisp 里扮演兩個角色,如果實參是一個空表,則函數(shù)?null
?返回?真
?。
> (null nil)
T
而如果實參是邏輯?假
?,則函數(shù)?not
?返回?真
?:
> (not nil)
T
null
?與?not
?做的是一樣的事情。
在 Common Lisp 里,最簡單的條件式是?if
?。通常接受三個實參:一個?test?表達(dá)式,一個?then?表達(dá)式和一個?else?表達(dá)式。若test
?表達(dá)式求值為邏輯?真
?,則對?then
?表達(dá)式求值,并返回這個值。若?test
?表達(dá)式求值為邏輯?假
?,則對?else
?表達(dá)式求值,并返回這個值:
> (if (listp '(a b c))
(+ 1 2)
(+ 5 6))
3
> (if (listp 27)
(+ 1 2)
(+ 5 6))
11
與?quote
?相同,?if
?是特殊的操作符。不能用函數(shù)來實現(xiàn),因為實參在函數(shù)調(diào)用時永遠(yuǎn)會被求值,而?if
?的特點是,只有最后兩個實參的其中一個會被求值。?if
?的最后一個實參是選擇性的。如果忽略它的話,缺省值是?nil
?:
> (if (listp 27)
(+ 1 2))
NIL
雖然?t
?是邏輯?真
?的缺省表示法,任何非?nil
?的東西,在邏輯的上下文里通通被視為?真
?。
> (if 27 1 2)
1
邏輯操作符?and
?和?or
?與條件式類似。兩者都接受任意數(shù)量的實參,但僅對能影響返回值的幾個實參求值。如果所有的實參都為?真
(即非?nil
?),那么?and
?會返回最后一個實參的值:
> (and t (+ 1 2))
3
如果其中一個實參為?假
?,那之后的所有實參都不會被求值。?or
?也是如此,只要碰到一個為?真
?的實參,就停止對之后所有的實參求值。
以上這兩個操作符稱為宏。宏和特殊的操作符一樣,可以繞過一般的求值規(guī)則。第十章解釋了如何編寫你自己的宏。
你可以用?defun
?來定義新函數(shù)。通常接受三個以上的實參:一個名字,一組用列表表示的實參,以及一個或多個組成函數(shù)體的表達(dá)式。我們可能會這樣定義?third
?:
> (defun our-third (x)
(car (cdr (cdr x))))
OUR-THIRD
第一個實參說明此函數(shù)的名稱將是?our-third
?。第二個實參,一個列表?(x)
?,說明這個函數(shù)會接受一個形參:?x
?。這樣使用的占位符符號叫做變量。當(dāng)變量代表了傳入函數(shù)的實參時,如這里的?x
?,又被叫做形參。
定義的剩余部分,?(car?(cdr?(cdr?x)))
?,即所謂的函數(shù)主體。它告訴 Lisp 該怎么計算此函數(shù)的返回值。所以調(diào)用一個?our-third
?函數(shù),對于我們作為實參傳入的任何?x
?,會返回?(car?(cdr?(cdr?x)))
?:
> (our-third '(a b c d))
C
既然我們已經(jīng)討論過了變量,理解符號是什么就更簡單了。符號是變量的名字,符號本身就是以對象的方式存在。這也是為什么符號,必須像列表一樣被引用。列表必須被引用,不然會被視為代碼。符號必須要被引用,不然會被當(dāng)作變量。
你可以把函數(shù)定義想成廣義版的 Lisp 表達(dá)式。下面的表達(dá)式測試?1
?和?4
?的和是否大于?3
?:
> (> (+ 1 4) 3)
T
通過將這些數(shù)字替換為變量,我們可以寫個函數(shù),測試任兩數(shù)之和是否大于第三個數(shù):
> (defun sum-greater (x y z)
(> (+ x y) z))
SUM-GREATER
> (sum-greater 1 4 3)
T
Lisp 不對程序、過程以及函數(shù)作區(qū)別。函數(shù)做了所有的事情(事實上,函數(shù)是語言的主要部分)。如果你想要把你的函數(shù)之一作為主函數(shù)(main?function),可以這么做,但平常你就能在頂層中調(diào)用任何函數(shù)。這表示當(dāng)你編程時,你可以把程序拆分成一小塊一小塊地來做調(diào)試。
上一節(jié)我們所定義的函數(shù),調(diào)用了別的函數(shù)來幫它們做事。比如?sum-greater
?調(diào)用了?+
?和?>
?。函數(shù)可以調(diào)用任何函數(shù),包括自己。自己調(diào)用自己的函數(shù)是遞歸的。 Common Lisp 函數(shù)?member
?,測試某個東西是否為列表的成員。下面是定義成遞歸函數(shù)的簡化版:
> (defun our-member (obj lst)
(if (null lst)
nil
(if (eql (car lst) obj)
lst
(our-member obj (cdr lst)))))
OUR-MEMBER
謂詞?eql
?測試它的兩個實參是否相等;此外,這個定義的所有東西我們之前都學(xué)過了。下面是運行的情形:
> (our-member 'b '(a b c))
(B C)
> (our-member 'z '(a b c))
NIL
下面是?our-member
?的定義對應(yīng)到英語的描述。為了知道一個對象?obj
?是否為列表?lst
?的成員,我們
- 首先檢查?
lst
?列表是否為空列表。如果是空列表,那?obj
?一定不是它的成員,結(jié)束。- 否則,若?
obj
?是列表的第一個元素時,則它是列表的成員。- 不然只有當(dāng)?
obj
?是列表其余部分的元素時,它才是列表的成員。
當(dāng)你想要了解遞歸函數(shù)是怎么工作時,把它翻成這樣的敘述有助于你理解。
起初,許多人覺得遞歸函數(shù)很難理解。大部分的理解難處,來自于對函數(shù)使用了錯誤的比喻。人們傾向于把函數(shù)理解為某種機(jī)器。原物料像實參一樣抵達(dá);某些工作委派給其它函數(shù);最后組裝起來的成品,被作為返回值運送出去。如果我們用這種比喻來理解函數(shù),那遞歸就自相矛盾了。機(jī)器怎可以把工作委派給自己?它已經(jīng)在忙碌中了。
較好的比喻是,把函數(shù)想成一個處理的過程。在過程里,遞歸是在自然不過的事情了。日常生活中我們經(jīng)??吹竭f歸的過程。舉例來說,假設(shè)一個歷史學(xué)家,對歐洲歷史上的人口變化感興趣。研究文獻(xiàn)的過程很可能是:
- 取得一個文獻(xiàn)的復(fù)本
- 尋找關(guān)于人口變化的資訊
- 如果這份文獻(xiàn)提到其它可能有用的文獻(xiàn),研究它們。
過程是很容易理解的,而且它是遞歸的,因為第三個步驟可能帶出一個或多個同樣的過程。
所以,別把?our-member
?想成是一種測試某個東西是否為列表成員的機(jī)器。而是把它想成是,決定某個東西是否為列表成員的規(guī)則。如果我們從這個角度來考慮函數(shù),那么遞歸的矛盾就不復(fù)存在了。
上一節(jié)我們所定義的?our-member
?以五個括號結(jié)尾。更復(fù)雜的函數(shù)定義更可能以七、八個括號結(jié)尾。剛學(xué) Lisp 的人看到這么多括號會感到氣餒。這叫人怎么讀這樣的程序,更不用說編了?怎么知道哪個括號該跟哪個匹配?
答案是,你不需要這么做。 Lisp 程序員用縮排來閱讀及編寫程序,而不是括號。當(dāng)他們在寫程序時,他們讓文字編輯器顯示哪個括號該與哪個匹配。任何好的文字編輯器,特別是 Lisp 系統(tǒng)自帶的,都應(yīng)該能做到括號匹配(paren-matching)。在這種編輯器中,當(dāng)你輸入一個括號時,編輯器指出與其匹配的那一個。如果你的編輯器不能匹配括號,別用了,想想如何讓它做到,因為沒有這個功能,你根本不可能編 Lisp 程序?[1]?。
有了好的編輯器之后,括號匹配不再會是問題。而且由于 Lisp 縮排有通用的慣例,閱讀程序也不是個問題。因為所有人都使用一樣的習(xí)慣,你可以忽略那些括號,通過縮排來閱讀程序。
任何有經(jīng)驗的 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排時,他就沒有問題了??梢院雎源蟛糠值睦ㄌ柖阅茏x懂它:
defun our-member (obj lst)
if null lst
nil
if eql (car lst) obj
lst
our-member obj (cdr lst)
事實上,這是你在紙上寫 Lisp 程序的實用方法。等輸入程序至計算機(jī)的時候,可以利用編輯器匹配括號的功能。
到目前為止,我們已經(jīng)利用頂層偷偷使用了 I/O 。對實際的交互程序來說,這似乎還是不太夠。在這一節(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
注意到有兩個東西被打印出來。第一行是?format
?印出來的。第二行是調(diào)用?format
?函數(shù)的返回值,就像平常頂層會打印出來的一樣。通常像?format
?這種函數(shù)不會直接在頂層調(diào)用,而是在程序內(nèi)部里使用,所以返回值不會被看到。
format
?的第一個實參?t
?,表示輸出被送到缺省的地方去。通常是頂層。第二個實參是一個用作輸出模版的字符串。在這字符串里,每一個?~A
?表示了被填入的位置,而?~%
?表示一個換行。這些被填入的位置依序由后面的實參填入。
標(biāo)準(zhǔn)的輸入函數(shù)是?read
?。當(dāng)沒有實參時,會讀取缺省的位置,通常是頂層。下面這個函數(shù),提示使用者輸入,并返回任何輸入的東西:
(defun askem (string)
(format t "~A" string)
(read))
它的行為如下:
> (askem "How old are you?")
How old are you?29
29
記住?read
?會一直永遠(yuǎn)等在這里,直到你輸入了某些東西,并且(通常要)按下回車。因此,不打印明確的提示信息是很不明智的,程序會給人已經(jīng)死機(jī)的印象,但其實它是在等待輸入。
第二件關(guān)于?read
?所需要知道的事是,它很強(qiáng)大:?read
?是一個完整的 Lisp 解析器(parser)。不僅是可以讀入字符,然后當(dāng)作字符串返回它們。它解析它所讀入的東西,并返回產(chǎn)生出來的 Lisp 對象。在上述的例子,它返回一個數(shù)字。
askem
?的定義雖然很短,但體現(xiàn)出一些我們在之前的函數(shù)沒看過的東西。函數(shù)主體可以有不只一個表達(dá)式。函數(shù)主體可以有任意數(shù)量的表達(dá)式。當(dāng)函數(shù)被調(diào)用時,會依序求值,函數(shù)會返回最后一個的值。
在之前的每一節(jié)中,我們堅持所謂“純粹的” Lisp ── 即沒有副作用的 Lisp 。副作用是指,表達(dá)式被求值后,對外部世界的狀態(tài)做了某些改變。當(dāng)我們對一個如?(+?1?2)
?這樣純粹的 Lisp 表達(dá)式求值時,沒有產(chǎn)生副作用。它只返回一個值。但當(dāng)我們調(diào)用?format
時,它不僅返回值,還印出了某些東西。這就是一種副作用。
當(dāng)我們想要寫沒有副作用的程序時,則定義多個表達(dá)式的函數(shù)主體就沒有意義了。最后一個表達(dá)式的值,會被當(dāng)成函數(shù)的返回值,而之前表達(dá)式的值都被舍棄了。如果這些表達(dá)式?jīng)]有副作用,你沒有任何理由告訴 Lisp ,為什么要去對它們求值。
let
?是一個最常用的 Common Lisp 的操作符之一,它讓你引入新的局部變量(local variable):
> (let ((x 1) (y 2))
(+ x y))
3
一個?let
?表達(dá)式有兩個部分。第一個部分是一組創(chuàng)建新變量的指令,指令的形式為?(variable expression)?。每一個變量會被賦予相對應(yīng)表達(dá)式的值。上述的例子中,我們創(chuàng)造了兩個變量,?x
?和?y
?,分別被賦予初始值?1
?和?2
?。這些變量只在?let
?的函數(shù)體內(nèi)有效。
一組變量與數(shù)值之后,是一個有表達(dá)式的函數(shù)體,表達(dá)式依序被求值。但這個例子里,只有一個表達(dá)式,調(diào)用?+
?函數(shù)。最后一個表達(dá)式的求值結(jié)果作為?let
?的返回值。以下是一個用?let
?所寫的,更有選擇性的?askem
?函數(shù):
(defun ask-number ()
(format t "Please enter a number. ")
(let ((val (read)))
(if (numberp val)
val
(ask-number))))
這個函數(shù)創(chuàng)建了變量?val
?來儲存?read
?所返回的對象。因為它知道該如何處理這個對象,函數(shù)可以先觀察你的輸入,再決定是否返回它。你可能猜到了,?numberp
?是一個謂詞,測試它的實參是否為數(shù)字。
如果使用者不是輸入一個數(shù)字,?ask-number
?會持續(xù)調(diào)用自己。最后得到一個只接受數(shù)字的函數(shù):
> (ask-number)
Please enter a number. a
Please enter a number. (ho hum)
Please enter a number. 52
52
我們已經(jīng)看過的這些變量都叫做局部變量。它們只在特定的上下文里有效。另外還有一種變量叫做全局變量(global variable),是在任何地方都是可視的。?[2]
你可以給?defparameter
?傳入符號和值,來創(chuàng)建一個全局變量:
> (defparameter *glob* 99)
*GLOB*
全局變量在任何地方都可以存取,除了在定義了相同名字的區(qū)域變量的表達(dá)式里。為了避免這種情形發(fā)生,通常我們在給全局變量命名時,以星號作開始與結(jié)束。剛才我們創(chuàng)造的變量可以念作 “星-glob-星” (star-glob-star)。
你也可以用?defconstant
?來定義一個全局的常量:
(defconstant limit (+ *glob* 1))
我們不需要給常量一個獨一無二的名字,因為如果有相同名字存在,就會有錯誤產(chǎn)生 (error)。如果你想要檢查某些符號,是否為一個全局變量或常量,使用?boundp
?函數(shù):
> (boundp '*glob*)
T
在 Common Lisp 里,最普遍的賦值操作符(assignment operator)是?setf
???梢杂脕斫o全局或局部變量賦值:
> (setf *glob* 98)
98
> (let ((n 10))
(setf n 2)
n)
2
如果?setf
?的第一個實參是符號(symbol),且符號不是某個局部變量的名字,則?setf
?把這個符號設(shè)為全局變量:
> (setf x (list 'a 'b 'c))
(A B C)
也就是說,通過賦值,你可以隱式地創(chuàng)建全局變量。 不過,一般來說,還是使用?defparameter
?明確地創(chuàng)建全局變量比較好。
你不僅可以給變量賦值。傳入?setf
?的第一個實參,還可以是表達(dá)式或變量名。在這種情況下,第二個實參的值被插入至第一個實參所引用的位置:
> (setf (car x) 'n)
N
> x
(N B C)
setf
?的第一個實參幾乎可以是任何引用到特定位置的表達(dá)式。所有這樣的操作符在附錄 D 中被標(biāo)注為 “可設(shè)置的”(“settable”)。你可以給?setf
?傳入(偶數(shù))個實參。一個這樣的表達(dá)式
(setf a 'b
c 'd
e 'f)
等同于依序調(diào)用三個單獨的?setf
?函數(shù):
(setf a 'b)
(setf c 'd)
(setf e 'f)
函數(shù)式編程意味著撰寫利用返回值而工作的程序,而不是修改東西。它是 Lisp 的主流范式。大部分 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
?從列表里移除一個對象?因為它不是這么做的。原來的表沒有被改變:
> lst
(C A R A T)
若你真的想從列表里移除某些東西怎么辦?在 Lisp 通常你這么做,把這個列表當(dāng)作實參,傳入某個函數(shù),并使用?setf
?來處理返回值。要移除所有在列表?x
?的?a
?,我們可以說:
(setf x (remove 'a x))
函數(shù)式編程本質(zhì)上意味著避免使用如?setf
?的函數(shù)。起初可能覺得這根本不可能,更遑論去做了。怎么可以只憑返回值來建立程序?
完全不用到副作用是很不方便的。然而,隨著你進(jìn)一步閱讀,會驚訝地發(fā)現(xiàn)需要用到副作用的地方很少。副作用用得越少,你就更上一層樓。
函數(shù)式編程最重要的優(yōu)點之一是,它允許交互式測試(interactive testing)。在純函數(shù)式的程序里,你可以測試每個你寫的函數(shù)。如果它返回你預(yù)期的值,你可以有信心它是對的。這額外的信心,集結(jié)起來,會產(chǎn)生巨大的差別。當(dāng)你改動了程序里的任何一個地方,會得到即時的改變。而這種即時的改變,使我們有一種新的編程風(fēng)格。類比于電話與信件,讓我們有一種新的通訊方式。
當(dāng)我們想重復(fù)做一些事情時,迭代比遞歸來得更自然。典型的例子是用迭代來產(chǎn)生某種表格。這個函數(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
DONE
do
?宏是 Common Lisp 里最基本的迭代操作符。和?let
?類似,?do
?可以創(chuàng)建變量,而第一個實參是一組變量的規(guī)格說明列表。每個元素可以是以下的形式
(variable initial update)
其中?variable?是一個符號,?initial?和?update?是表達(dá)式。最初每個變量會被賦予?initial?表達(dá)式的值;每一次迭代時,會被賦予update?表達(dá)式的值。在?show-squares
?函數(shù)里,?do
?只創(chuàng)建了一個變量?i
?。第一次迭代時,?i
?被賦與?start
?的值,在接下來的迭代里,?i
?的值每次增加?1
?。
第二個傳給?do
?的實參可包含一個或多個表達(dá)式。第一個表達(dá)式用來測試迭代是否結(jié)束。在上面的例子中,測試表達(dá)式是?(>?iend)
?。接下來在列表中的表達(dá)式會依序被求值,直到迭代結(jié)束。而最后一個值會被當(dāng)作?do
?的返回值來返回。所以?show-squares
?總是返回?done
?。
do
?的剩余參數(shù)組成了循環(huán)的函數(shù)體。在每次迭代時,函數(shù)體會依序被求值。在每次迭代過程里,變量被更新,檢查終止測試條件,接著(若測試失?。┣笾岛瘮?shù)體。
作為對比,以下是遞歸版本的?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
?。?progn
?接受任意數(shù)量的表達(dá)式,依序求值,并返回最后一個表達(dá)式的值。
為了處理某些特殊情況, Common Lisp 有更簡單的迭代操作符。舉例來說,要遍歷列表的元素,你可能會使用?dolist
?。以下函數(shù)返回列表的長度:
(defun our-length (lst)
(let ((len 0))
(dolist (obj lst)
(setf len (+ len 1)))
len))
這里?dolist
?接受這樣形式的實參(variable expression),跟著一個具有表達(dá)式的函數(shù)主體。函數(shù)主體會被求值,而變量相繼與表達(dá)式所返回的列表元素綁定。因此上面的循環(huán)說,對于列表?lst
?里的每一個?obj
?,遞增?len
?。很顯然這個函數(shù)的遞歸版本是:
(defun our-length (lst)
(if (null lst)
0
(+ (our-length (cdr lst)) 1)))
也就是說,如果列表是空表,則長度為?0
?;否則長度就是對列表取?cdr
?的長度加一。遞歸版本的?our-length
?比較易懂,但由于它不是尾遞歸(tail-recursive)的形式 (見 13.2 節(jié)),效率不是那么高。
函數(shù)在 Lisp 里,和符號、字符串或列表一樣,是稀松平常的對象。如果我們把函數(shù)的名字傳給?function
?,它會返回相關(guān)聯(lián)的對象。和?quote
?類似,?function
?是一個特殊操作符,所以我們無需引用(quote)它的實參:
> (function +)
#<Compiled-Function + 17BA4E>
這看起來很奇怪的返回值,是在典型的 Common Lisp 實現(xiàn)里,函數(shù)可能的打印表示法。
到目前為止,我們僅討論過,不管是 Lisp 打印它們,還是我們輸入它們,看起來都是一樣的對象。但這個慣例對函數(shù)不適用。一個像是?+
?的內(nèi)置函數(shù) ,在內(nèi)部可能是一段機(jī)器語言代碼(machine language code)。每個 Common Lisp 實現(xiàn),可以選擇任何它喜歡的外部表示法(external representation)。
如同我們可以用?'
?作為?quote
?的縮寫,也可以用?#'
?作為?function
?的縮寫:
> #'+
#<Compiled-Function + 17BA4E>
這個縮寫稱之為升引號(sharp-quote)。
和別種對象類似,可以把函數(shù)當(dāng)作實參傳入。有個接受函數(shù)作為實參的函數(shù)是?apply
?。apply
?接受一個函數(shù)和實參列表,并返回把傳入函數(shù)應(yīng)用在實參列表的結(jié)果:
> (apply #'+ '(1 2 3))
6
> (+ 1 2 3)
6
apply
?可以接受任意數(shù)量的實參,只要最后一個實參是列表即可:
> (apply #'+ 1 2 '(3 4 5))
15
函數(shù)?funcall
?做的是一樣的事情,但不需要把實參包裝成列表。
> (funcall #'+ 1 2 3)
6
什么是?lambda
??
lambda
?表達(dá)式里的?lambda
?不是一個操作符。而只是個符號。 在早期的 Lisp 方言里,?lambda
?存在的原因是:由于函數(shù)在內(nèi)部是用列表來表示, 因此辨別列表與函數(shù)的方法,就是檢查第一個元素是否為?lambda
?。
在 Common Lisp 里,你可以用列表來表達(dá)函數(shù), 函數(shù)在內(nèi)部會被表示成獨特的函數(shù)對象。因此不再需要?lambda?了。 如果需要把函數(shù)記為
((x) (+ x 100))
而不是
(lambda (x) (+ x 100))
也是可以的。
但 Lisp 程序員習(xí)慣用符號?lambda
?,來撰寫函數(shù), 因此 Common Lisp 為了傳統(tǒng),而保留了?lambda
?。
defun
?宏,創(chuàng)建一個函數(shù)并給函數(shù)命名。但函數(shù)不需要有名字,而且我們不需要?defun
?來定義他們。和大多數(shù)的 Lisp 對象一樣,我們可以直接引用函數(shù)。
要直接引用整數(shù),我們使用一系列的數(shù)字;要直接引用一個函數(shù),我們使用所謂的lambda 表達(dá)式。一個?lambda
?表達(dá)式是一個列表,列表包含符號?lambda
?,接著是形參列表,以及由零個或多個表達(dá)式所組成的函數(shù)體。
下面的?lambda
?表達(dá)式,表示一個接受兩個數(shù)字并返回兩者之和的函數(shù):
(lambda (x y)
(+ x y))
列表?(x?y)
?是形參列表,跟在它后面的是函數(shù)主體。
一個?lambda
?表達(dá)式可以作為函數(shù)名。和普通的函數(shù)名稱一樣, lambda 表達(dá)式也可以是函數(shù)調(diào)用的第一個元素,
> ((lambda (x) (+ x 100)) 1)
101
而通過在?lambda
?表達(dá)式前面貼上?#'
?,我們得到對應(yīng)的函數(shù),
> (funcall #'(lambda (x) (+ x 100))
1)
lambda
?表示法除上述用途以外,還允許我們使用匿名函數(shù)。
Lisp 處理類型的方法非常靈活。在很多語言里,變量是有類型的,得聲明變量的類型才能使用它。在 Common Lisp 里,數(shù)值才有類型,而變量沒有。你可以想像每個對象,都貼有一個標(biāo)明其類型的標(biāo)簽。這種方法叫做顯式類型(manifest typing)。你不需要聲明變量的類型,因為變量可以存放任何類型的對象。
雖然從來不需要聲明類型,但出于效率的考量,你可能會想要聲明變量的類型。類型聲明在第 13.3 節(jié)時討論。
Common Lisp 的內(nèi)置類型,組成了一個類別的層級。對象總是不止屬于一個類型。舉例來說,數(shù)字 27 的類型,依普遍性的增加排序,依序是?fixnum
?、?integer
?、?rational
?、?real
?、?number
?、?atom
?和?t
?類型。(數(shù)值類型將在第 9 章討論。)類型?t
?是所有類型的基類(supertype)。所以每個對象都屬于?t
?類型。
函數(shù)?typep
?接受一個對象和一個類型,然后判定對象是否為該類型,是的話就返回真:
> (typep 27 'integer)
T
我們會在遇到各式內(nèi)置類型時來討論它們。
本章僅談到 Lisp 的表面。然而,一種非比尋常的語言形象開始出現(xiàn)了。首先,這個語言用單一的語法,來表達(dá)所有的程序結(jié)構(gòu)。語法基于列表,列表是一種 Lisp 對象。函數(shù)本身也是 Lisp 對象,函數(shù)能用列表來表示。而 Lisp 本身就是 Lisp 程序。幾乎所有你定義的函數(shù),與內(nèi)置的 Lisp 函數(shù)沒有任何區(qū)別。
如果你對這些概念還不太了解,不用擔(dān)心。 Lisp 介紹了這么多新穎的概念,在你能駕馭它們之前,得花時間去熟悉它們。不過至少要了解一件事:在這些概念當(dāng)中,有著優(yōu)雅到令人吃驚的概念。
Richard Gabriel?曾經(jīng)半開玩笑的說, C 是拿來寫 Unix 的語言。我們也可以說, Lisp 是拿來寫 Lisp 的語言。但這是兩種不同的論述。一個可以用自己編寫的語言和一種適合編寫某些特定類型應(yīng)用的語言,是有著本質(zhì)上的不同。這開創(chuàng)了新的編程方法:你不但在語言之中編程,還把語言改善成適合程序的語言。如果你想了解 Lisp 編程的本質(zhì),理解這個概念是個好的開始。
quote
?操作符有自己的求值規(guī)則,它完封不動地返回實參。cons
?,它創(chuàng)建一個列表;?car
?,它返回列表的第一個元素;以及?cdr
?,它返回第一個元素之后的所有東西。t
?表示邏輯?真
?,而?nil
?表示邏輯?假
?。在邏輯的上下文里,任何非?nil
?的東西都視為?真
??;镜臈l件式是?if
?。?and
?與?or
?是相似的條件式。defun
?來定義新的函數(shù)。read
?,它包含了一個完整的 Lisp 語法分析器,以及?format
?,它通過字符串模板來產(chǎn)生輸出。let
?來創(chuàng)造新的局部變量,用?defparameter
?來創(chuàng)造全局變量。setf
?。它的第一個實參可以是一個表達(dá)式。do
?。(a) (+ (- 5 1) (+ 3 7))
(b) (list 1 (+ 2 3))
(c) (if (listp 1) (+ 1 2) (+ 3 4))
(d) (list (and (listp 3) t) (+ 1 2))
(a?b?c)
?的?cons?表達(dá)式
?。car
?與?cdr
?來定義一個函數(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))))))
x
?該是什么,才會得到相同的結(jié)果?(a) > (car (x (cdr '(a (b c) d))))
B
(b) > (x 13 (/ 1 0))
13
(c) > (x #'list 1 nil)
(1)
給出函數(shù)的迭代與遞歸版本:
接受一個列表,并返回?a
?在列表里所出現(xiàn)的次數(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))))))
腳注
[1] | 在 vi,你可以用 :set sm 來啟用括號匹配。在 Emacs,M-x lisp-mode 是一個啟用的好方法。
[2] | 真正的區(qū)別是詞法變量(lexical)與特殊變量(special variable),但到第六章才會討論這個主題。
更多建議: