第二章:歡迎來到 Lisp

2018-02-24 15:50 更新

本章的目的是讓你盡快開始編程。本章結(jié)束時,你會掌握足夠多的 Common Lisp 知識來開始寫程序。

2.1 形式 (Form)

人可以通過實踐來學(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á)所有的概念。

2.2 求值 (Evaluation)

上一小節(jié)中,我們在頂層輸入表達(dá)式,然后 Lisp 顯示它們的值。在這節(jié)里我們深入理解一下表達(dá)式是如何被求值的。

在 Lisp 里,?+?是函數(shù),然而如?(+?2?3)?的表達(dá)式,是函數(shù)調(diào)用。

當(dāng) Lisp 對函數(shù)調(diào)用求值時,它做下列兩個步驟:

  1. 首先從左至右對實參求值。在這個例子當(dāng)中,實參對自身求值,所以實參的值分別是?2?跟?3?。
  2. 實參的值傳入以操作符命名的函數(shù)。在這個例子當(dāng)中,將?2?跟?3?傳給?+?函數(shù),返回?5?。

如果實參本身是函數(shù)調(diào)用的話,上述規(guī)則同樣適用。以下是當(dāng)?(/?(-?7?1)?(-?4?2))?表達(dá)式被求值時的情形:

  1. Lisp 對?(-?7?1)?求值:?7?求值為?7?,?1?求值為?1?,它們被傳給函數(shù)?-?,返回?6?。
  2. Lisp 對?(-?4?2)?求值:?4?求值為?4?,?2?求值為?2?,它們被傳給函數(shù)?-?,返回?2?。
  3. 數(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ù)很有用。

2.3 數(shù)據(jù) (Data)

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?是對自身求值的。

2.4 列表操作 (List Operations)

用函數(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

2.5 真與假 (Truth)

在 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ī)則。第十章解釋了如何編寫你自己的宏。

2.6 函數(shù) (Functions)

你可以用?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)試。

2.7 遞歸 (Recursion)

上一節(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?的成員,我們

  1. 首先檢查?lst?列表是否為空列表。如果是空列表,那?obj?一定不是它的成員,結(jié)束。
  2. 否則,若?obj?是列表的第一個元素時,則它是列表的成員。
  3. 不然只有當(dāng)?obj?是列表其余部分的元素時,它才是列表的成員。

當(dāng)你想要了解遞歸函數(shù)是怎么工作時,把它翻成這樣的敘述有助于你理解。

起初,許多人覺得遞歸函數(shù)很難理解。大部分的理解難處,來自于對函數(shù)使用了錯誤的比喻。人們傾向于把函數(shù)理解為某種機(jī)器。原物料像實參一樣抵達(dá);某些工作委派給其它函數(shù);最后組裝起來的成品,被作為返回值運送出去。如果我們用這種比喻來理解函數(shù),那遞歸就自相矛盾了。機(jī)器怎可以把工作委派給自己?它已經(jīng)在忙碌中了。

較好的比喻是,把函數(shù)想成一個處理的過程。在過程里,遞歸是在自然不過的事情了。日常生活中我們經(jīng)??吹竭f歸的過程。舉例來說,假設(shè)一個歷史學(xué)家,對歐洲歷史上的人口變化感興趣。研究文獻(xiàn)的過程很可能是:

  1. 取得一個文獻(xiàn)的復(fù)本
  2. 尋找關(guān)于人口變化的資訊
  3. 如果這份文獻(xiàn)提到其它可能有用的文獻(xiàn),研究它們。

過程是很容易理解的,而且它是遞歸的,因為第三個步驟可能帶出一個或多個同樣的過程。

所以,別把?our-member?想成是一種測試某個東西是否為列表成員的機(jī)器。而是把它想成是,決定某個東西是否為列表成員的規(guī)則。如果我們從這個角度來考慮函數(shù),那么遞歸的矛盾就不復(fù)存在了。

2.8 閱讀 Lisp (Reading Lisp)

上一節(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ī)的時候,可以利用編輯器匹配括號的功能。

2.9 輸入輸出 (Input and Output)

到目前為止,我們已經(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 ,為什么要去對它們求值。

2.10 變量 (Variables)

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

2.11 賦值 (Assignment)

在 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)

2.12 函數(shù)式編程 (Functional Programming)

函數(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)格。類比于電話與信件,讓我們有一種新的通訊方式。

2.13 迭代 (Iteration)

當(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é)),效率不是那么高。

2.14 函數(shù)作為對象 (Functions as Objects)

函數(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ù)。

2.15 類型 (Types)

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)置類型時來討論它們。

2.16 展望 (Looking Forward)

本章僅談到 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ì),理解這個概念是個好的開始。

Chapter 2 總結(jié) (Summary)

  1. Lisp 是一種交互式語言。如果你在頂層輸入一個表達(dá)式, Lisp 會顯示它的值。
  2. Lisp 程序由表達(dá)式組成。表達(dá)式可以是原子,或一個由操作符跟著零個或多個實參的列表。前序表示法代表操作符可以有任意數(shù)量的實參。
  3. Common Lisp 函數(shù)調(diào)用的求值規(guī)則: 依序?qū)崊淖笾劣仪笾?,接著把它們的值傳入由操作符表示的函?shù)。?quote?操作符有自己的求值規(guī)則,它完封不動地返回實參。
  4. 除了一般的數(shù)據(jù)類型, Lisp 還有符號跟列表。由于 Lisp 程序是用列表來表示的,很輕松就能寫出能編程的程序。
  5. 三個基本的列表函數(shù)是?cons?,它創(chuàng)建一個列表;?car?,它返回列表的第一個元素;以及?cdr?,它返回第一個元素之后的所有東西。
  6. 在 Common Lisp 里,?t?表示邏輯??,而?nil?表示邏輯??。在邏輯的上下文里,任何非?nil?的東西都視為???;镜臈l件式是?if?。?and?與?or?是相似的條件式。
  7. Lisp 主要由函數(shù)所組成。可以用?defun?來定義新的函數(shù)。
  8. 自己調(diào)用自己的函數(shù)是遞歸的。一個遞歸函數(shù)應(yīng)該要被想成是過程,而不是機(jī)器。
  9. 括號不是問題,因為程序員通過縮排來閱讀與編寫 Lisp 程序。
  10. 基本的 I/O 函數(shù)是?read?,它包含了一個完整的 Lisp 語法分析器,以及?format?,它通過字符串模板來產(chǎn)生輸出。
  11. 你可以用?let?來創(chuàng)造新的局部變量,用?defparameter?來創(chuàng)造全局變量。
  12. 賦值操作符是?setf?。它的第一個實參可以是一個表達(dá)式。
  13. 函數(shù)式編程代表避免產(chǎn)生副作用,也是 Lisp 的主導(dǎo)思維。
  14. 基本的迭代操作符是?do?。
  15. 函數(shù)是 Lisp 的對象??梢员划?dāng)成實參傳入,并且可以用 lambda 表達(dá)式來表示。
  16. 在 Lisp 里,是數(shù)值才有類型,而不是變量。

Chapter 2 習(xí)題 (Exercises)

  1. 描述下列表達(dá)式求值之后的結(jié)果:
(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?表達(dá)式?。
  2. 使用?car?與?cdr?來定義一個函數(shù),返回一個列表的第四個元素。
  3. 定義一個函數(shù),接受兩個實參,返回兩者當(dāng)中較大的那個。
  4. 這些函數(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))))))
  1. 下列表達(dá)式,?x?該是什么,才會得到相同的結(jié)果?
(a) > (car (x (cdr '(a (b c) d))))
    B
(b) > (x 13 (/ 1 0))
    13
(c) > (x #'list 1 nil)
    (1)
  1. 只使用本章所介紹的操作符,定義一個函數(shù),它接受一個列表作為實參,如果有一個元素是列表時,就返回真。
  2. 給出函數(shù)的迭代與遞歸版本:

  3. 接受一個正整數(shù),并打印出數(shù)字?jǐn)?shù)量的點。
  4. 接受一個列表,并返回?a?在列表里所出現(xiàn)的次數(shù)。

  5. 一位朋友想寫一個函數(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),但到第六章才會討論這個主題。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號