本章的目的是讓你盡快開始編程。本章結束時,你會掌握足夠多的 Common Lisp 知識來開始寫程序。
[TOC]
## 2.1 形式 (Form)[](http://acl.readthedocs.org/en/latest/zhCN/ch2-cn.html#form "Permalink to this headline")
人可以通過實踐來學習一件事,這對于 Lisp 來說特別有效,因為 Lisp 是一門交互式的語言。任何 Lisp 系統都含有一個交互式的前端,叫做*頂層*(toplevel)。你在頂層輸入 Lisp 表達式,而系統會顯示它們的值。
Lisp 通常會打印一個提示符告訴你,它正在等待你的輸入。許多 Common Lisp 的實現用?`>`?作為頂層提示符。本書也沿用這個符號。
一個最簡單的 Lisp 表達式是整數。如果我們在提示符后面輸入?`1`?,
~~~
> 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`?這樣的數原子,要么是包在括號里,由零個或多個表達式所構成的列表。以下是合法的 Lisp 表達式:
~~~
2 (+ 2 3) (+ 2 3 4) (/ (- 7 1) (- 4 2))
~~~
稍后我們將理解到,所有的 Lisp 程序都采用這種形式。而像是 C 這種語言,有著更復雜的語法:算術表達式采用中序表示法;函數調用采用某種前序表示法,實參用逗號隔開;表達式用分號隔開;而一段程序用大括號隔開。
在 Lisp 里,我們用單一的表示法,來表達所有的概念。
## 2.2 求值 (Evaluation)[](http://acl.readthedocs.org/en/latest/zhCN/ch2-cn.html#evaluation "Permalink to this headline")
上一小節中,我們在頂層輸入表達式,然后 Lisp 顯示它們的值。在這節里我們深入理解一下表達式是如何被求值的。
在 Lisp 里,?`+`?是函數,然而如?`(+?2?3)`?的表達式,是函數調用。
當 Lisp 對函數調用求值時,它做下列兩個步驟:
> 1. 首先從左至右對實參求值。在這個例子當中,實參對自身求值,所以實參的值分別是?`2`?跟?`3`?。
> 2. 實參的值傳入以操作符命名的函數。在這個例子當中,將?`2`?跟?`3`?傳給?`+`?函數,返回?`5`?。
如果實參本身是函數調用的話,上述規則同樣適用。以下是當?`(/?(-?7?1)?(-?4?2))`?表達式被求值時的情形:
> 1. Lisp 對?`(-?7?1)`?求值:?`7`?求值為?`7`?,?`1`?求值為?`1`?,它們被傳給函數?`-`?,返回?`6`?。
> 2. Lisp 對?`(-?4?2)`?求值:?`4`?求值為?`4`?,?`2`?求值為?`2`?,它們被傳給函數?`-`?,返回?`2`?。
> 3. 數值?`6`?與?`2`?被傳入函數?`/`?,返回?`3`?。
但不是所有的 Common Lisp 操作符都是函數,不過大部分是。函數調用都是這么求值。由左至右對實參求值,將它們的數值傳入函數,來返回整個表達式的值。這稱為 Common Lisp 的求值規則。
逃離麻煩
如果你試著輸入 Lisp 不能理解的東西,它會打印一個錯誤訊息,接著帶你到一種叫做*中斷循環*(break loop)的頂層。 中斷循環給予有經驗的程序員一個機會,來找出錯誤的原因,不過最初你只會想知道如何從中斷循環中跳出。 如何返回頂層取決于你所使用的 Common Lisp 實現。在這個假定的實現環境中,輸入?`:abort`?跳出:
~~~
> (/ 1 0)
Error: Division by zero
Options: :abort, :backtrace
>> :abort
>
~~~
附錄 A 演示了如何調試 Lisp 程序,并給出一些常見的錯誤例子。
一個不遵守 Common Lisp 求值規則的操作符是?`quote`?。?`quote`?是一個特殊的操作符,意味著它自己有一套特別的求值規則。這個規則就是:什么也不做。?`quote`?操作符接受一個實參,并完封不動地返回它。
~~~
> (quote (+ 3 5))
(+ 3 5)
~~~
為了方便起見,Common Lisp 定義?`'`?作為?`quote`?的縮寫。你可以在任何的表達式前,貼上一個?`'`?,與調用?`quote`?是同樣的效果:
~~~
> '(+ 3 5)
(+ 3 5)
~~~
使用縮寫?`'`?比使用整個?`quote`?表達式更常見。
Lisp 提供?`quote`?作為一種*保護*表達式不被求值的方式。下一節將解釋為什么這種保護很有用。
## 2.3 數據 (Data)[](http://acl.readthedocs.org/en/latest/zhCN/ch2-cn.html#data "Permalink to this headline")
Lisp 提供了所有在其他語言找的到的,以及其他語言所找不到的數據類型。一個我們已經使用過的類型是*整數*(integer),整數用一系列的數字來表示,比如:?`256`?。另一個 Common Lisp 與多數語言有關,并很常見的數據類型是*字符串*(string),字符串用一系列被雙引號包住的字符串表示,比如:?`"ora?et?labora"`?[[3]](http://acl.readthedocs.org/en/latest/zhCN/ch2-cn.html#id2)?。整數與字符串一樣,都是對自身求值的。
| [[3]](http://acl.readthedocs.org/en/latest/zhCN/ch2-cn.html#id1) | “ora et labora” 是拉丁文,意思是禱告與工作。 |
有兩個通常在別的語言所找不到的 Lisp 數據類型是*符號*(symbol)與*列表*(lists),*符號*是英語的單詞 (words)。無論你怎么輸入,通常會被轉換為大寫:
~~~
> '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`?是對自身求值的。
## 2.4 列表操作 (List Operations)[](http://acl.readthedocs.org/en/latest/zhCN/ch2-cn.html#list-operations "Permalink to this headline")
用函數?`cons`?來構造列表。如果傳入的第二個實參是列表,則返回由兩個實參所構成的新列表,新列表為第一個實參加上第二個實參:
~~~
> (cons 'a '(b c d))
(A B C D)
~~~
可以通過把新元素建立在空表之上,來構造一個新列表。上一節所看到的函數?`list`?,不過就是一個把幾個元素加到?`nil`?上的快捷方式:
~~~
> (cons 'a (cons 'b nil))
(A B)
> (list 'a 'b)
(A B)
~~~
取出列表元素的基本函數是?`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
~~~
## 2.5 真與假 (Truth)[](http://acl.readthedocs.org/en/latest/zhCN/ch2-cn.html#truth "Permalink to this headline")
在 Common Lisp 里,符號?`t`?是表示邏輯?`真`?的缺省值。與?`nil`?相同,?`t`?也是對自身求值的。如果實參是一個列表,則函數?`listp`返回?`真`?:
~~~
> (listp '(a b c))
T
~~~
函數的返回值將會被解釋成邏輯?`真`?或邏輯?`假`?時,則稱此函數為謂詞(*predicate*)。在 Common Lisp 里,謂詞的名字通常以?`p`結尾。
邏輯?`假`?在 Common Lisp 里,用?`nil`?,即空表來表示。如果我們傳給?`listp`?的實參不是列表,則返回?`nil`?。
~~~
> (listp 27)
NIL
~~~
由于?`nil`?在 Common Lisp 里扮演兩個角色,如果實參是一個空表,則函數?`null`?返回?`真`?。
~~~
> (null nil)
T
~~~
而如果實參是邏輯?`假`?,則函數?`not`?返回?`真`?:
~~~
> (not nil)
T
~~~
`null`?與?`not`?做的是一樣的事情。
在 Common Lisp 里,最簡單的條件式是?`if`?。通常接受三個實參:一個?*test*?表達式,一個?*then*?表達式和一個?*else*?表達式。若`test`?表達式求值為邏輯?`真`?,則對?`then`?表達式求值,并返回這個值。若?`test`?表達式求值為邏輯?`假`?,則對?`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)
(+ 1 2))
NIL
~~~
雖然?`t`?是邏輯?`真`?的缺省表示法,任何非?`nil`?的東西,在邏輯的上下文里通通被視為?`真`?。
~~~
> (if 27 1 2)
1
~~~
邏輯操作符?`and`?和?`or`?與條件式類似。兩者都接受任意數量的實參,但僅對能影響返回值的幾個實參求值。如果所有的實參都為?`真`(即非?`nil`?),那么?`and`?會返回最后一個實參的值:
~~~
> (and t (+ 1 2))
3
~~~
如果其中一個實參為?`假`?,那之后的所有實參都不會被求值。?`or`?也是如此,只要碰到一個為?`真`?的實參,就停止對之后所有的實參求值。
以上這兩個操作符稱為*宏*。宏和特殊的操作符一樣,可以繞過一般的求值規則。第十章解釋了如何編寫你自己的宏。
## 2.6 函數 (Functions)[](http://acl.readthedocs.org/en/latest/zhCN/ch2-cn.html#functions "Permalink to this headline")
你可以用?`defun`?來定義新函數。通常接受三個以上的實參:一個名字,一組用列表表示的實參,以及一個或多個組成函數體的表達式。我們可能會這樣定義?`third`?:
~~~
> (defun our-third (x)
(car (cdr (cdr x))))
OUR-THIRD
~~~
第一個實參說明此函數的名稱將是?`our-third`?。第二個實參,一個列表?`(x)`?,說明這個函數會接受一個形參:?`x`?。這樣使用的占位符符號叫做*變量*。當變量代表了傳入函數的實參時,如這里的?`x`?,又被叫做*形參*。
定義的剩余部分,?`(car?(cdr?(cdr?x)))`?,即所謂的函數主體。它告訴 Lisp 該怎么計算此函數的返回值。所以調用一個?`our-third`?函數,對于我們作為實參傳入的任何?`x`?,會返回?`(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)
T
~~~
Lisp 不對程序、過程以及函數作區別。函數做了所有的事情(事實上,函數是語言的主要部分)。如果你想要把你的函數之一作為主函數(*main*?function),可以這么做,但平常你就能在頂層中調用任何函數。這表示當你編程時,你可以把程序拆分成一小塊一小塊地來做調試。
## 2.7 遞歸 (Recursion)[](http://acl.readthedocs.org/en/latest/zhCN/ch2-cn.html#recursion "Permalink to this headline")
上一節我們所定義的函數,調用了別的函數來幫它們做事。比如?`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)))))
OUR-MEMBER
~~~
謂詞?`eql`?測試它的兩個實參是否相等;此外,這個定義的所有東西我們之前都學過了。下面是運行的情形:
~~~
> (our-member 'b '(a b c))
(B C)
> (our-member 'z '(a b c))
NIL
~~~
下面是?`our-member`?的定義對應到英語的描述。為了知道一個對象?`obj`?是否為列表?`lst`?的成員,我們
> 1. 首先檢查?`lst`?列表是否為空列表。如果是空列表,那?`obj`?一定不是它的成員,結束。
> 2. 否則,若?`obj`?是列表的第一個元素時,則它是列表的成員。
> 3. 不然只有當?`obj`?是列表其余部分的元素時,它才是列表的成員。
當你想要了解遞歸函數是怎么工作時,把它翻成這樣的敘述有助于你理解。
起初,許多人覺得遞歸函數很難理解。大部分的理解難處,來自于對函數使用了錯誤的比喻。人們傾向于把函數理解為某種機器。原物料像實參一樣抵達;某些工作委派給其它函數;最后組裝起來的成品,被作為返回值運送出去。如果我們用這種比喻來理解函數,那遞歸就自相矛盾了。機器怎可以把工作委派給自己?它已經在忙碌中了。
較好的比喻是,把函數想成一個處理的過程。在過程里,遞歸是在自然不過的事情了。日常生活中我們經常看到遞歸的過程。舉例來說,假設一個歷史學家,對歐洲歷史上的人口變化感興趣。研究文獻的過程很可能是:
> 1. 取得一個文獻的復本
> 2. 尋找關于人口變化的資訊
> 3. 如果這份文獻提到其它可能有用的文獻,研究它們。
過程是很容易理解的,而且它是遞歸的,因為第三個步驟可能帶出一個或多個同樣的過程。
所以,別把?`our-member`?想成是一種測試某個東西是否為列表成員的機器。而是把它想成是,決定某個東西是否為列表成員的規則。如果我們從這個角度來考慮函數,那么遞歸的矛盾就不復存在了。
## 2.8 閱讀 Lisp (Reading Lisp)[](http://acl.readthedocs.org/en/latest/zhCN/ch2-cn.html#lisp-reading-lisp "Permalink to this headline")
上一節我們所定義的?`our-member`?以五個括號結尾。更復雜的函數定義更可能以七、八個括號結尾。剛學 Lisp 的人看到這么多括號會感到氣餒。這叫人怎么讀這樣的程序,更不用說編了?怎么知道哪個括號該跟哪個匹配?
答案是,你不需要這么做。 Lisp 程序員用縮排來閱讀及編寫程序,而不是括號。當他們在寫程序時,他們讓文字編輯器顯示哪個括號該與哪個匹配。任何好的文字編輯器,特別是 Lisp 系統自帶的,都應該能做到括號匹配(paren-matching)。在這種編輯器中,當你輸入一個括號時,編輯器指出與其匹配的那一個。如果你的編輯器不能匹配括號,別用了,想想如何讓它做到,因為沒有這個功能,你根本不可能編 Lisp 程序?[[1]](http://acl.readthedocs.org/en/latest/zhCN/ch2-cn.html#id5)?。
有了好的編輯器之后,括號匹配不再會是問題。而且由于 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 程序的實用方法。等輸入程序至計算機的時候,可以利用編輯器匹配括號的功能。
## 2.9 輸入輸出 (Input and Output)[](http://acl.readthedocs.org/en/latest/zhCN/ch2-cn.html#input-and-output "Permalink to this headline")
到目前為止,我們已經利用頂層偷偷使用了 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 解析器(parser)。不僅是可以讀入字符,然后當作字符串返回它們。它解析它所讀入的東西,并返回產生出來的 Lisp 對象。在上述的例子,它返回一個數字。
`askem`?的定義雖然很短,但體現出一些我們在之前的函數沒看過的東西。函數主體可以有不只一個表達式。函數主體可以有任意數量的表達式。當函數被調用時,會依序求值,函數會返回最后一個的值。
在之前的每一節中,我們堅持所謂“純粹的” Lisp ── 即沒有副作用的 Lisp 。副作用是指,表達式被求值后,對外部世界的狀態做了某些改變。當我們對一個如?`(+?1?2)`?這樣純粹的 Lisp 表達式求值時,沒有產生副作用。它只返回一個值。但當我們調用?`format`時,它不僅返回值,還印出了某些東西。這就是一種副作用。
當我們想要寫沒有副作用的程序時,則定義多個表達式的函數主體就沒有意義了。最后一個表達式的值,會被當成函數的返回值,而之前表達式的值都被舍棄了。如果這些表達式沒有副作用,你沒有任何理由告訴 Lisp ,為什么要去對它們求值。
## 2.10 變量 (Variables)[](http://acl.readthedocs.org/en/latest/zhCN/ch2-cn.html#variables "Permalink to this headline")
`let`?是一個最常用的 Common Lisp 的操作符之一,它讓你引入新的局部變量(local variable):
~~~
> (let ((x 1) (y 2))
(+ x y))
3
~~~
一個?`let`?表達式有兩個部分。第一個部分是一組創建新變量的指令,指令的形式為?*(variable expression)*?。每一個變量會被賦予相對應表達式的值。上述的例子中,我們創造了兩個變量,?`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
~~~
我們已經看過的這些變量都叫做局部變量。它們只在特定的上下文里有效。另外還有一種變量叫做全局變量(global variable),是在任何地方都是可視的。?[[2]](http://acl.readthedocs.org/en/latest/zhCN/ch2-cn.html#id6)
你可以給?`defparameter`?傳入符號和值,來創建一個全局變量:
~~~
> (defparameter *glob* 99)
*GLOB*
~~~
全局變量在任何地方都可以存取,除了在定義了相同名字的區域變量的表達式里。為了避免這種情形發生,通常我們在給全局變量命名時,以星號作開始與結束。剛才我們創造的變量可以念作 “星-glob-星” (star-glob-star)。
你也可以用?`defconstant`?來定義一個全局的常量:
~~~
(defconstant limit (+ *glob* 1))
~~~
我們不需要給常量一個獨一無二的名字,因為如果有相同名字存在,就會有錯誤產生 (error)。如果你想要檢查某些符號,是否為一個全局變量或常量,使用?`boundp`?函數:
~~~
> (boundp '*glob*)
T
~~~
## 2.11 賦值 (Assignment)[](http://acl.readthedocs.org/en/latest/zhCN/ch2-cn.html#assignment "Permalink to this headline")
在 Common Lisp 里,最普遍的賦值操作符(assignment operator)是?`setf`?。可以用來給全局或局部變量賦值:
~~~
> (setf *glob* 98)
98
> (let ((n 10))
(setf n 2)
n)
2
~~~
如果?`setf`?的第一個實參是符號(symbol),且符號不是某個局部變量的名字,則?`setf`?把這個符號設為全局變量:
~~~
> (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)
~~~
## 2.12 函數式編程 (Functional Programming)[](http://acl.readthedocs.org/en/latest/zhCN/ch2-cn.html#functional-programming "Permalink to this headline")
函數式編程意味著撰寫利用返回值而工作的程序,而不是修改東西。它是 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`?來處理返回值。要移除所有在列表?`x`?的?`a`?,我們可以說:
~~~
(setf x (remove 'a x))
~~~
函數式編程本質上意味著避免使用如?`setf`?的函數。起初可能覺得這根本不可能,更遑論去做了。怎么可以只憑返回值來建立程序?
完全不用到副作用是很不方便的。然而,隨著你進一步閱讀,會驚訝地發現需要用到副作用的地方很少。副作用用得越少,你就更上一層樓。
函數式編程最重要的優點之一是,它允許交互式測試(interactive testing)。在純函數式的程序里,你可以測試每個你寫的函數。如果它返回你預期的值,你可以有信心它是對的。這額外的信心,集結起來,會產生巨大的差別。當你改動了程序里的任何一個地方,會得到即時的改變。而這種即時的改變,使我們有一種新的編程風格。類比于電話與信件,讓我們有一種新的通訊方式。
## 2.13 迭代 (Iteration)[](http://acl.readthedocs.org/en/latest/zhCN/ch2-cn.html#iteration "Permalink to this headline")
當我們想重復做一些事情時,迭代比遞歸來得更自然。典型的例子是用迭代來產生某種表格。這個函數
~~~
(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
DONE
~~~
`do`?宏是 Common Lisp 里最基本的迭代操作符。和?`let`?類似,?`do`?可以創建變量,而第一個實參是一組變量的規格說明列表。每個元素可以是以下的形式
~~~
(variable initial update)
~~~
其中?*variable*?是一個符號,?*initial*?和?*update*?是表達式。最初每個變量會被賦予?*initial*?表達式的值;每一次迭代時,會被賦予*update*?表達式的值。在?`show-squares`?函數里,?`do`?只創建了一個變量?`i`?。第一次迭代時,?`i`?被賦與?`start`?的值,在接下來的迭代里,?`i`?的值每次增加?`1`?。
第二個傳給?`do`?的實參可包含一個或多個表達式。第一個表達式用來測試迭代是否結束。在上面的例子中,測試表達式是?`(>?iend)`?。接下來在列表中的表達式會依序被求值,直到迭代結束。而最后一個值會被當作?`do`?的返回值來返回。所以?`show-squares`?總是返回?`done`?。
`do`?的剩余參數組成了循環的函數體。在每次迭代時,函數體會依序被求值。在每次迭代過程里,變量被更新,檢查終止測試條件,接著(若測試失敗)求值函數體。
作為對比,以下是遞歸版本的?`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`?接受任意數量的表達式,依序求值,并返回最后一個表達式的值。
為了處理某些特殊情況, Common Lisp 有更簡單的迭代操作符。舉例來說,要遍歷列表的元素,你可能會使用?`dolist`?。以下函數返回列表的長度:
~~~
(defun our-length (lst)
(let ((len 0))
(dolist (obj lst)
(setf len (+ len 1)))
len))
~~~
這里?`dolist`?接受這樣形式的實參*(variable expression)*,跟著一個具有表達式的函數主體。函數主體會被求值,而變量相繼與表達式所返回的列表元素綁定。因此上面的循環說,對于列表?`lst`?里的每一個?`obj`?,遞增?`len`?。很顯然這個函數的遞歸版本是:
~~~
(defun our-length (lst)
(if (null lst)
0
(+ (our-length (cdr lst)) 1)))
~~~
也就是說,如果列表是空表,則長度為?`0`?;否則長度就是對列表取?`cdr`?的長度加一。遞歸版本的?`our-length`?比較易懂,但由于它不是尾遞歸(tail-recursive)的形式 (見 13.2 節),效率不是那么高。
## 2.14 函數作為對象 (Functions as Objects)[](http://acl.readthedocs.org/en/latest/zhCN/ch2-cn.html#functions-as-objects "Permalink to this headline")
函數在 Lisp 里,和符號、字符串或列表一樣,是稀松平常的對象。如果我們把函數的名字傳給?`function`?,它會返回相關聯的對象。和?`quote`?類似,?`function`?是一個特殊操作符,所以我們無需引用(quote)它的實參:
~~~
> (function +)
#<Compiled-Function + 17BA4E>
~~~
這看起來很奇怪的返回值,是在典型的 Common Lisp 實現里,函數可能的打印表示法。
到目前為止,我們僅討論過,不管是 Lisp 打印它們,還是我們輸入它們,看起來都是一樣的對象。但這個慣例對函數不適用。一個像是?`+`?的內置函數 ,在內部可能是一段機器語言代碼(machine language code)。每個 Common Lisp 實現,可以選擇任何它喜歡的外部表示法(external representation)。
如同我們可以用?`'`?作為?`quote`?的縮寫,也可以用?`#'`?作為?`function`?的縮寫:
~~~
> #'+
#<Compiled-Function + 17BA4E>
~~~
這個縮寫稱之為升引號(sharp-quote)。
和別種對象類似,可以把函數當作實參傳入。有個接受函數作為實參的函數是?`apply`?。`apply`?接受一個函數和實參列表,并返回把傳入函數應用在實參列表的結果:
~~~
> (apply #'+ '(1 2 3))
6
> (+ 1 2 3)
6
~~~
`apply`?可以接受任意數量的實參,只要最后一個實參是列表即可:
~~~
> (apply #'+ 1 2 '(3 4 5))
15
~~~
函數?`funcall`?做的是一樣的事情,但不需要把實參包裝成列表。
~~~
> (funcall #'+ 1 2 3)
6
~~~
什么是?`lambda`??
`lambda`?表達式里的?`lambda`?不是一個操作符。而只是個符號。 在早期的 Lisp 方言里,?`lambda`?存在的原因是:由于函數在內部是用列表來表示, 因此辨別列表與函數的方法,就是檢查第一個元素是否為?`lambda`?。
在 Common Lisp 里,你可以用列表來表達函數, 函數在內部會被表示成獨特的函數對象。因此不再需要?lambda?了。 如果需要把函數記為
>
>
>
>
> ~~~
> ((x) (+ x 100))
> ~~~
>
>
>
>
而不是
>
>
>
>
> ~~~
> (lambda (x) (+ x 100))
> ~~~
>
>
>
>
也是可以的。
但 Lisp 程序員習慣用符號?`lambda`?,來撰寫函數, 因此 Common Lisp 為了傳統,而保留了?`lambda`?。
`defun`?宏,創建一個函數并給函數命名。但函數不需要有名字,而且我們不需要?`defun`?來定義他們。和大多數的 Lisp 對象一樣,我們可以直接引用函數。
要直接引用整數,我們使用一系列的數字;要直接引用一個函數,我們使用所謂的*lambda 表達式*。一個?`lambda`?表達式是一個列表,列表包含符號?`lambda`?,接著是形參列表,以及由零個或多個表達式所組成的函數體。
下面的?`lambda`?表達式,表示一個接受兩個數字并返回兩者之和的函數:
~~~
(lambda (x y)
(+ x y))
~~~
列表?`(x?y)`?是形參列表,跟在它后面的是函數主體。
一個?`lambda`?表達式可以作為函數名。和普通的函數名稱一樣, lambda 表達式也可以是函數調用的第一個元素,
~~~
> ((lambda (x) (+ x 100)) 1)
101
~~~
而通過在?`lambda`?表達式前面貼上?`#'`?,我們得到對應的函數,
~~~
> (funcall #'(lambda (x) (+ x 100))
1)
~~~
`lambda`?表示法除上述用途以外,還允許我們使用匿名函數。
## 2.15 類型 (Types)[](http://acl.readthedocs.org/en/latest/zhCN/ch2-cn.html#types "Permalink to this headline")
Lisp 處理類型的方法非常靈活。在很多語言里,變量是有類型的,得聲明變量的類型才能使用它。在 Common Lisp 里,數值才有類型,而變量沒有。你可以想像每個對象,都貼有一個標明其類型的標簽。這種方法叫做*顯式類型*(*manifest typing*)。你不需要聲明變量的類型,因為變量可以存放任何類型的對象。
雖然從來不需要聲明類型,但出于效率的考量,你可能會想要聲明變量的類型。類型聲明在第 13.3 節時討論。
Common Lisp 的內置類型,組成了一個類別的層級。對象總是不止屬于一個類型。舉例來說,數字 27 的類型,依普遍性的增加排序,依序是?`fixnum`?、?`integer`?、?`rational`?、?`real`?、?`number`?、?`atom`?和?`t`?類型。(數值類型將在第 9 章討論。)類型?`t`?是所有類型的基類(supertype)。所以每個對象都屬于?`t`?類型。
函數?`typep`?接受一個對象和一個類型,然后判定對象是否為該類型,是的話就返回真:
~~~
> (typep 27 'integer)
T
~~~
我們會在遇到各式內置類型時來討論它們。
## 2.16 展望 (Looking Forward)[](http://acl.readthedocs.org/en/latest/zhCN/ch2-cn.html#looking-forward "Permalink to this headline")
本章僅談到 Lisp 的表面。然而,一種非比尋常的語言形象開始出現了。首先,這個語言用單一的語法,來表達所有的程序結構。語法基于列表,列表是一種 Lisp 對象。函數本身也是 Lisp 對象,函數能用列表來表示。而 Lisp 本身就是 Lisp 程序。幾乎所有你定義的函數,與內置的 Lisp 函數沒有任何區別。
如果你對這些概念還不太了解,不用擔心。 Lisp 介紹了這么多新穎的概念,在你能駕馭它們之前,得花時間去熟悉它們。不過至少要了解一件事:在這些概念當中,有著優雅到令人吃驚的概念。
[Richard Gabriel](http://en.wikipedia.org/wiki/Richard_P._Gabriel)?曾經半開玩笑的說, C 是拿來寫 Unix 的語言。我們也可以說, Lisp 是拿來寫 Lisp 的語言。但這是兩種不同的論述。一個可以用自己編寫的語言和一種適合編寫某些特定類型應用的語言,是有著本質上的不同。這開創了新的編程方法:你不但在語言之中編程,還把語言改善成適合程序的語言。如果你想了解 Lisp 編程的本質,理解這個概念是個好的開始。
## Chapter 2 總結 (Summary)[](http://acl.readthedocs.org/en/latest/zhCN/ch2-cn.html#chapter-2-summary "Permalink to this headline")
1. Lisp 是一種交互式語言。如果你在頂層輸入一個表達式, Lisp 會顯示它的值。
2. Lisp 程序由表達式組成。表達式可以是原子,或一個由操作符跟著零個或多個實參的列表。前序表示法代表操作符可以有任意數量的實參。
3. Common Lisp 函數調用的求值規則: 依序對實參從左至右求值,接著把它們的值傳入由操作符表示的函數。?`quote`?操作符有自己的求值規則,它完封不動地返回實參。
4. 除了一般的數據類型, Lisp 還有符號跟列表。由于 Lisp 程序是用列表來表示的,很輕松就能寫出能編程的程序。
5. 三個基本的列表函數是?`cons`?,它創建一個列表;?`car`?,它返回列表的第一個元素;以及?`cdr`?,它返回第一個元素之后的所有東西。
6. 在 Common Lisp 里,?`t`?表示邏輯?`真`?,而?`nil`?表示邏輯?`假`?。在邏輯的上下文里,任何非?`nil`?的東西都視為?`真`?。基本的條件式是?`if`?。?`and`?與?`or`?是相似的條件式。
7. Lisp 主要由函數所組成。可以用?`defun`?來定義新的函數。
8. 自己調用自己的函數是遞歸的。一個遞歸函數應該要被想成是過程,而不是機器。
9. 括號不是問題,因為程序員通過縮排來閱讀與編寫 Lisp 程序。
10. 基本的 I/O 函數是?`read`?,它包含了一個完整的 Lisp 語法分析器,以及?`format`?,它通過字符串模板來產生輸出。
11. 你可以用?`let`?來創造新的局部變量,用?`defparameter`?來創造全局變量。
12. 賦值操作符是?`setf`?。它的第一個實參可以是一個表達式。
13. 函數式編程代表避免產生副作用,也是 Lisp 的主導思維。
14. 基本的迭代操作符是?`do`?。
15. 函數是 Lisp 的對象。可以被當成實參傳入,并且可以用 lambda 表達式來表示。
16. 在 Lisp 里,是數值才有類型,而不是變量。
## Chapter 2 習題 (Exercises)[](http://acl.readthedocs.org/en/latest/zhCN/ch2-cn.html#chapter-2-exercises "Permalink to this headline")
1. 描述下列表達式求值之后的結果:
~~~
(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))
~~~
1. 給出 3 種不同表示?`(a?b?c)`?的?`cons?表達式`?。
2. 使用?`car`?與?`cdr`?來定義一個函數,返回一個列表的第四個元素。
3. 定義一個函數,接受兩個實參,返回兩者當中較大的那個。
4. 這些函數做了什么?
~~~
(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))))))
~~~
1. 下列表達式,?`x`?該是什么,才會得到相同的結果?
~~~
(a) > (car (x (cdr '(a (b c) d))))
B
(b) > (x 13 (/ 1 0))
13
(c) > (x #'list 1 nil)
(1)
~~~
1. 只使用本章所介紹的操作符,定義一個函數,它接受一個列表作為實參,如果有一個元素是列表時,就返回真。
2. 給出函數的迭代與遞歸版本:
1. 接受一個正整數,并打印出數字數量的點。
2. 接受一個列表,并返回?`a`?在列表里所出現的次數。
1. 一位朋友想寫一個函數,返回列表里所有非?`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))))))
~~~
腳注
[[1]](http://acl.readthedocs.org/en/latest/zhCN/ch2-cn.html#id3) | 在 vi,你可以用 :set sm 來啟用括號匹配。在 Emacs,M-x lisp-mode 是一個啟用的好方法。
[[2]](http://acl.readthedocs.org/en/latest/zhCN/ch2-cn.html#id4) | 真正的區別是詞法變量(lexical)與特殊變量(special variable),但到第六章才會討論這個主題。