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