第二章:歡迎來(lái)到 Lisp

2018-02-24 15:50 更新

本章的目的是讓你盡快開(kāi)始編程。本章結(jié)束時(shí),你會(huì)掌握足夠多的 Common Lisp 知識(shí)來(lái)開(kāi)始寫程序。

2.1 形式 (Form)

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

2.2 求值 (Evaluation)

上一小節(jié)中,我們?cè)陧攲虞斎氡磉_(dá)式,然后 Lisp 顯示它們的值。在這節(jié)里我們深入理解一下表達(dá)式是如何被求值的。

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

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

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

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

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

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

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ì)自身求值的。

2.4 列表操作 (List Operations)

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

2.5 真與假 (Truth)

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

2.6 函數(shù) (Functions)

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

2.7 遞歸 (Recursion)

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

  1. 首先檢查?lst?列表是否為空列表。如果是空列表,那?obj?一定不是它的成員,結(jié)束。
  2. 否則,若?obj?是列表的第一個(gè)元素時(shí),則它是列表的成員。
  3. 不然只有當(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ò)程很可能是:

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

過(guò)程是很容易理解的,而且它是遞歸的,因?yàn)榈谌齻€(gè)步驟可能帶出一個(gè)或多個(gè)同樣的過(guò)程。

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

2.8 閱讀 Lisp (Reading Lisp)

上一節(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)的功能。

2.9 輸入輸出 (Input and Output)

到目前為止,我們已經(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ì)它們求值。

2.10 變量 (Variables)

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

2.11 賦值 (Assignment)

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

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

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

2.13 迭代 (Iteration)

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

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

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

2.15 類型 (Types)

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)討論它們。

2.16 展望 (Looking Forward)

本章僅談到 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)始。

Chapter 2 總結(jié) (Summary)

  1. Lisp 是一種交互式語(yǔ)言。如果你在頂層輸入一個(gè)表達(dá)式, Lisp 會(huì)顯示它的值。
  2. Lisp 程序由表達(dá)式組成。表達(dá)式可以是原子,或一個(gè)由操作符跟著零個(gè)或多個(gè)實(shí)參的列表。前序表示法代表操作符可以有任意數(shù)量的實(shí)參。
  3. Common Lisp 函數(shù)調(diào)用的求值規(guī)則: 依序?qū)?shí)參從左至右求值,接著把它們的值傳入由操作符表示的函數(shù)。?quote?操作符有自己的求值規(guī)則,它完封不動(dòng)地返回實(shí)參。
  4. 除了一般的數(shù)據(jù)類型, Lisp 還有符號(hào)跟列表。由于 Lisp 程序是用列表來(lái)表示的,很輕松就能寫出能編程的程序。
  5. 三個(gè)基本的列表函數(shù)是?cons?,它創(chuàng)建一個(gè)列表;?car?,它返回列表的第一個(gè)元素;以及?cdr?,它返回第一個(gè)元素之后的所有東西。
  6. 在 Common Lisp 里,?t?表示邏輯??,而?nil?表示邏輯??。在邏輯的上下文里,任何非?nil?的東西都視為???;镜臈l件式是?if?。?and?與?or?是相似的條件式。
  7. Lisp 主要由函數(shù)所組成??梢杂?defun?來(lái)定義新的函數(shù)。
  8. 自己調(diào)用自己的函數(shù)是遞歸的。一個(gè)遞歸函數(shù)應(yīng)該要被想成是過(guò)程,而不是機(jī)器。
  9. 括號(hào)不是問(wèn)題,因?yàn)槌绦騿T通過(guò)縮排來(lái)閱讀與編寫 Lisp 程序。
  10. 基本的 I/O 函數(shù)是?read?,它包含了一個(gè)完整的 Lisp 語(yǔ)法分析器,以及?format?,它通過(guò)字符串模板來(lái)產(chǎn)生輸出。
  11. 你可以用?let?來(lái)創(chuàng)造新的局部變量,用?defparameter?來(lái)創(chuàng)造全局變量。
  12. 賦值操作符是?setf?。它的第一個(gè)實(shí)參可以是一個(gè)表達(dá)式。
  13. 函數(shù)式編程代表避免產(chǎn)生副作用,也是 Lisp 的主導(dǎo)思維。
  14. 基本的迭代操作符是?do?。
  15. 函數(shù)是 Lisp 的對(duì)象??梢员划?dāng)成實(shí)參傳入,并且可以用 lambda 表達(dá)式來(lái)表示。
  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?來(lái)定義一個(gè)函數(shù),返回一個(gè)列表的第四個(gè)元素。
  3. 定義一個(gè)函數(shù),接受兩個(gè)實(shí)參,返回兩者當(dāng)中較大的那個(gè)。
  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?該是什么,才會(huì)得到相同的結(jié)果?
(a) > (car (x (cdr '(a (b c) d))))
    B
(b) > (x 13 (/ 1 0))
    13
(c) > (x #'list 1 nil)
    (1)
  1. 只使用本章所介紹的操作符,定義一個(gè)函數(shù),它接受一個(gè)列表作為實(shí)參,如果有一個(gè)元素是列表時(shí),就返回真。
  2. 給出函數(shù)的迭代與遞歸版本:

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

  5. 一位朋友想寫一個(gè)函數(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è)主題。

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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)