2.11 構(gòu)造(CONS)
CONS函數(shù)是創(chuàng)造內(nèi)存單位的函數(shù),這個函數(shù)接受兩個輸入,返回一個指向內(nèi)存單元的指針,這個創(chuàng)造出來的內(nèi)存單元。CAR指向第一個輸入,CDR指向第二個輸入。函數(shù)名CONS就是construct構(gòu)造的縮寫。
加入我們用括號表達式來解釋CONS函數(shù)的行為,那就是在列表之前插入了一個元素。例如把字符串A插入到列表(B C D)的首部、

另一個例子:把字符串SINK插入到列表(OR SWIM)中。

有個養(yǎng)一個函數(shù)GREET(問候)是把字符串HELLO插入到任何對象之前形成列表:

為了切實理解CONS的運行,最好使用內(nèi)部表示法來思考。CONS是一個極其簡單的函數(shù),他并不知道“在列表的前面”是具體什么意思,(請注意,在計算機內(nèi)部是沒有圓括號標(biāo)示的)。所有的CONS所做的事情就只是創(chuàng)造一個新的內(nèi)存單元。但是計入第二個輸入是一個列表,長度為n,那么新的列表將會是長度為n+1。請看圖2-1,CONS函數(shù)返回一個內(nèi)存單元的指針,在效果上看,返回的是一個長度加1的列表。

2.11.1 CONS函數(shù)和空列表
既然已知NIL是空列表,那么一個對象和NIL作為輸入,CONS函數(shù)輸出的就是一個單元素列表。

通過觀察結(jié)果的內(nèi)部表示可以確認(rèn),結(jié)果的CAR是指向字符串FROB,CDR是指向NIL,所以CONS函數(shù)實際上是創(chuàng)造了一個列表。

還有另外一個例子,可以吧FROB用NIL來替代。

如果是從打印表達來看的話,將某個對象和NIL組合在一起,其實就是加上了一層圓括號。

2.11.2 使用CONS函數(shù)創(chuàng)造嵌套列表
CONS函數(shù)的第一輸入如果是一個非空列表,結(jié)果就將會是一個嵌套函數(shù),是一個多層次的額內(nèi)存單元結(jié)構(gòu)。

2.11.3 CONS函數(shù)直接創(chuàng)造列表
假設(shè)我們現(xiàn)在想要直接創(chuàng)造列表(FOO BAR BAZ)。我們會先將字符串和NIL組合創(chuàng)造出列表(BAZ)。

然后再把BAR加上:

最后加上FOO:

類似于下圖的模型,我們直接創(chuàng)造了列表(FOO BAR BAZ)。

如果從橫向看這個圖,那么就會發(fā)現(xiàn)這和列表(FOO BAR BAZ)的內(nèi)存結(jié)構(gòu)圖一模一樣,這也是給出了一個線索,為什么CONS函數(shù)和內(nèi)存單元(cons cell)使用了相同的名字。
練習(xí)題
2.18 寫一個雙輸入函數(shù),使用CONS函數(shù)將他們組成一個列表。
2.12 CONS函數(shù)和CAR/CDR的互相轉(zhuǎn)換
一個有趣的現(xiàn)象就是CONS函數(shù)和CAR/CDR之間的相互轉(zhuǎn)換。給出一個函數(shù)x。x的CAR和CDR組合在一起就是x本身。例如,列表x的CAR是字符串A,x的cCDR是列表(E I O U),那列表x肯定就是(A E I O U)。
這種互相轉(zhuǎn)換的形式還可以使用等式來表達。
x = CONS of (CAR of x) and (CDR of x)
然而,這種轉(zhuǎn)換關(guān)系只有在,列表非空的情況下成立。當(dāng)x是NIL的時候,列表x的CAR和CDR也是NIL。加入我們將x的CAR和CDR部分組合在一起,也即是NIL和NIL組合,昌盛的不是空列表NIL而是列表(NIL)。這就是意味著,他們是相同的,但是就我們所知,NIL和(NIL)是不同的。這也提醒我們NIL并不是一個尋常的列表。現(xiàn)在的既有現(xiàn)實就是,這個互換性只支持非空列表,也就是至少列表中需要含有一個元素。
2.13 列表
把一些元素組合起來狗在一個列表在Lisp中是一個很普遍的操作,LIST函數(shù)就是實現(xiàn)這個功能的內(nèi)建函數(shù)。LIST函數(shù)接受任何數(shù)量的輸入,并把它們結(jié)合成一個列表作為結(jié)果輸出。LIST函數(shù)構(gòu)造了一個以NIL為結(jié)束的內(nèi)存單元鏈條,元素數(shù)量就是輸入的數(shù)量。圖片2-2顯示了這一個具體過程。

回顧CONS函數(shù)總是構(gòu)成一個新的內(nèi)存單位,行為是降低一個輸入加入到第二個輸入當(dāng)中。而LIST函數(shù)是完全創(chuàng)造一個新的內(nèi)存單元鏈條。在括號表達式中,表現(xiàn)為用圓括號將所有輸入包括進來,無論輸入的數(shù)目是多少。LIST函數(shù)的輸出結(jié)果總是比輸入要多一層的括號。

LIST函數(shù)分配三個新的內(nèi)存單元:

填充CAR的指針

填充CDR指針并形成鏈條,返回第一個內(nèi)存單元的指針

圖片2-2 LIST函數(shù)如何形成一個新的列表

LIST函數(shù)的工作實際上是構(gòu)成一個新的內(nèi)存單元鏈條,CAR部分是指向各個輸入的指針,而輸出的結(jié)果就是第一個內(nèi)存單位的指針。下面是一些例子:

下面是一個叫做BLURT函數(shù)的定義,接受兩個輸入并且使用它們填充句子中的空缺。

BLURT函數(shù)舉例:

我們現(xiàn)在來看看CONS函數(shù)了LIST函數(shù)的區(qū)別,CONS函數(shù)夠早的是一個內(nèi)存單元,而LIST函數(shù)構(gòu)造的是一個新的內(nèi)存單元鏈條,并且不論輸入數(shù)量。


理解LIST函數(shù)的拎一個方式就是將其看做CONS函數(shù)的級聯(lián)調(diào)用。

練習(xí)題
2.19 寫出下列計算過程的結(jié)果

2.14 替換一個列表中的首元素
假設(shè)我們想要用字符串WHAT來替換一個列表中的首元素。首先REST函數(shù)可以被用來提取不包括首元素在內(nèi)的子列表,然后CONS函數(shù)再把字符串WHAT加入到首元素的位子上。這樣一個自定義的函數(shù)被稱為SAY-WHAT。


(TAKE A NAP)輸入REST函數(shù)之后輸出的結(jié)果是(A NAP)。將字符串WHAT組合到和這個列表中就得到了新的列表(WHAT A NAP)。
如所見,SAY-WHAT函數(shù)并不是真的替換了列表的首元素。所做的事情是產(chǎn)生了一個新的列表然后將生層的內(nèi)存單元的CDR指針指向了這個列表。最后的結(jié)果如下圖所示:

練習(xí)題
2.20 下列計算過程返回的結(jié)果是?

2.21 定義一個四輸入函數(shù),返回一個兩元素的嵌套函數(shù)。第一個元素師前兩個輸入構(gòu)成的列表,第二個元素是后兩個輸入構(gòu)成的列表。
2.22 假設(shè)我們要定義一個叫做DUO-CONS 的函數(shù),在列表之前加上兩個元素。請注意一般的CONS函數(shù)式在列表前面加上一個元素。DUO-CONS函數(shù)將會接受三個輸入。例如,假設(shè)輸入是字符串PATRICK,字符串SEYMOUR,還有列表(MARVIN),那么DUO-CONS函數(shù)的輸出將會是列表(PATRICK SEYMOUR MARVIN)。請定義這樣一個函數(shù)。
2.23 TWO-DEEPER函數(shù)的作用是給輸入加上兩層括號。例如,輸入是TWO-DEEPER,輸出是((TWO-DEEPER)),輸入是(BOW WOW)輸出就是(((BOW WOW)))。請使用LIST函數(shù)來定義這個TWO-DEEPER函數(shù)。并使用CONS函數(shù)來定義另一個版本。
2.24 什么內(nèi)建函數(shù)可以將字符串NIGHT從列表(((GOOD)) ((NIGHT)))中提取出來?
2.15 LIST斷言
假如輸入是列表LISTP輸出T,如果不是列表就輸出NIL。

如果輸入是內(nèi)存單元(cons cell)CONSP斷言就會返回T。CONSP幾乎等同于LISTP,只是在對待NIL作為輸入的時候有所不同。NIL是一個列表,但卻不是一個內(nèi)存單元。

如果輸入不是一個內(nèi)存單元,那函數(shù)ATOM就會返回T。函數(shù)ATOM和函數(shù)CONSP剛好是相對立的,同一個輸入的情況下,一個返回T的同時,另一個必定返回NIL。

單詞ATOM來源于希臘語atomos,意思是不可分割的,原子。數(shù)字和字符串因為不能再拆分所以是不可分割的,而非空列表不是不可分割的。FIRST函數(shù)和REST函數(shù)就可以拆分他們。
NULL斷言在輸入是NIL的情況下返回T。這個行為和NOT斷言是一樣的。根據(jù)慣例,Lisp程序員在準(zhǔn)備邏輯操作的時候使用NOT,比如把true改成false,false改成true。放加測一個列表是不是為空的時候使用NULL。
小結(jié)
本章介紹了在Lisp中最豐富的數(shù)據(jù)類型,列表。列表同時具有打印形式和內(nèi)部存儲形式,列表可以包括字符串,數(shù)字或者其他列表作為元素。
我們可以使用CAR或者CDR(FIRST和REST)來把列表拆分,也可以使用CONS函數(shù)和LIST函數(shù)來構(gòu)造列表。LENGTH函數(shù)可以計算在列表中的元素數(shù)目,這個數(shù)目也就是列表的頂層元素的個數(shù)。
CAR和CDR的要點在于:
- CAR和CDR只接受列表作為輸入
- FIRST函數(shù)和REST函數(shù)與CAR和CDR相同。
- SECOND和THIRD等同于CADR和CADDR.
- Common Lisp提供了C____R的內(nèi)建函數(shù),其中間由四位組合。
字符串NIL也有一些重要事項:
- NIL是一個字符串,在Lisp中是表達no或者false的唯一手段。
- NIL是一個列表,空列表,長度是0。
- NIL在Lisp中式唯一一個既是字符串又是列表的對象。
- NIL是一個內(nèi)存單元鏈條的結(jié)束標(biāo)記。當(dāng)列表以打印形式來體現(xiàn),按照慣例,鏈條的結(jié)尾的NIL是被省略的。
- NIL和()在內(nèi)部形式中式等同的。
- NIL的CAR和CDR被定義為NIL。
復(fù)習(xí)題
2.25 為什么cons cell和CONS會有相同的名字?
2.26 下面兩個函數(shù)在給予相同輸入(A B C)的時候會怎樣操作?

2.27 在什么時候,一個列表包含的內(nèi)存單元要比這個列表所有的元素還多?
2.28 試過只是使用CAR和CDR,有沒有可能定義出一個函數(shù)來返回列表的最后一個元素,無論這個列表有多長?請解釋。
本章涉及的函數(shù)
列表函數(shù):FIRST,SECOND,THIRD,FOURTH,CAR,CDR,CONS,LIST,LENGTH.
CAR和CDR的組合:CADR,CADDR等等。
斷言:LISTP,CONSP,.ATOM,NULL。
第二章進階話題
2.16 列表的單數(shù)算術(shù)運算
列表是可以用在單數(shù)(位數(shù)是1)算術(shù)運算上的。在這個系統(tǒng)中,數(shù)字式被表示成標(biāo)記符號組成的列表,就像一個在監(jiān)獄中的犯人在牢房的墻上刻下記號來數(shù)過去了幾天。數(shù)字1就是一個標(biāo)記符號,2就是兩個標(biāo)記符號,以此類推。數(shù)字0被標(biāo)記為沒有標(biāo)記。我們不考慮附屬的情況。
假設(shè)使用x作為標(biāo)記符號,我們可以將數(shù)字寫成標(biāo)記符號x組成的列表。
0就是NIL
1就是(x)
2就是(x x)
3就是(x x x)
鑒于已經(jīng)定義的單數(shù),我們可以研究一下,列表操作函數(shù)來操作他們。REST函數(shù)在數(shù)字鐘減去1.就像SUB1函數(shù)定義的一樣,從自然數(shù)中減去1,

1減去1的情況就是得到0:

但是0減去1的道德并不是-1,請注意我們定義的個位數(shù)運算時沒有負(fù)數(shù)的。

LENGTH函數(shù)可以講這個標(biāo)記列表轉(zhuǎn)換成為自然數(shù):

不是所有列表原始函數(shù)都能在一元運算中使用。CAR函數(shù)不可以。然而,將費原始函數(shù)運用在一元運算中是可能實現(xiàn)的。
練習(xí)題
2.29 寫一個函數(shù)UNARY-ADD1來給個位數(shù)加1、
2.30 CDDR會對藝一元數(shù)字有什么操作?
2.31 寫一個UNARY-ZEROP斷言。
2.32 寫一個UNARY-GREATERP斷言,功能類似于>斷言。
2.33 CAR也可以被看做一個一元數(shù)字的斷言。不是返回T或者NIL,CAR返回的是X或者NIL。請注意,X或者其他任何非NIL的對象在Lisp中都被看做true。當(dāng)一元數(shù)字輸入的CAR函數(shù)的時候會有什么問題?
2.17 非列表單元結(jié)構(gòu)
一個嚴(yán)格意義上的列表是用NIL來作為結(jié)尾的。作為管理,括號表達式的時候是省略括號的。所以下列表達就能被寫成(A B C)。

還有另一種內(nèi)存單位結(jié)構(gòu)并不是嚴(yán)格意義上的列表,因為在結(jié)尾并不是指向NIL。這樣的結(jié)構(gòu)該如何用括號表達式來表達呢?

開始打印一個列表的括號表達式的時候,Lisp最先從做便當(dāng)與那括號開始打印,但后是依次的個股元素,并且用空格分割開來。加入這個列表是用NIL來作為結(jié)尾,Lisp將會用右括號來結(jié)尾,如果不是NIL來結(jié)尾,在打印右括號之前先打印一個空格,一個小數(shù)點,再來一個空格,然后是一個院子對象結(jié)束。這樣的列表稱作點式列表(dotted list),并不是嚴(yán)格意義上的列表。
(A B C . D)
到現(xiàn)在為止,產(chǎn)生不以NIL為結(jié)尾的內(nèi)存結(jié)構(gòu)的方法只有通過CONS函數(shù)。

CONS函數(shù)的結(jié)果被稱為點對(dotted pair)。括號表達式寫成(A . B),內(nèi)部形式是這樣

一個點對是一個CDR指向不為NIL的內(nèi)存單元。點列表(A B . C)包括兩個內(nèi)存單元,構(gòu)成如下:

在內(nèi)部形式中,(A B . C)是這樣

雖然LIST函數(shù)式筆CONS函數(shù)更加方便的構(gòu)成列表的方法,但是LIST函數(shù)只能構(gòu)成一般的列表,也就是NIL結(jié)尾的列表,不能構(gòu)成點式列表,點式列表就必須用到CONS函數(shù)。
練習(xí)題
2.34 寫一個表達式,來構(gòu)造列表(A B C . D),使用CONS函數(shù)的級聯(lián)調(diào)用。
2.35 畫出點式列表((A . B) (C . D))的內(nèi)部表達,寫一個表達式來狗仔這個列表。
2.18 循環(huán)列表
點式列表看上去可能有點奇怪,但是更奇怪的也是有的,就是要介紹的循環(huán)列表(circular list):

如果計算機想要展示上圖中的列表,那么一些問題就可能出現(xiàn),根據(jù)各個打印設(shè)備的不同有所不同,這個一會兒再討論。計算機可能陷入沒有盡頭的循環(huán)當(dāng)中,或者嘗試打印一部分的列表,使用省略號等等:
(A B C A B C A B ...)
這個種方式是不正確的,因為他表現(xiàn)了列表包含超過十個以上的元素, 但是實際上列表只有三個元素。
Common Lisp提供了完整而正確的方法來解決循環(huán)結(jié)構(gòu),使用的而方法就是以井號#為基礎(chǔ)的,井號等位標(biāo)記法(sharp-equal notation)。基本上,為了表示循環(huán)結(jié)構(gòu),我們需要一個方法,來給一個內(nèi)存單元賦予一個標(biāo)簽,然后稍后才能再找回到這里。(例如,上圖中的循環(huán)列表,第三個內(nèi)存單元的CDR指向回到了第一個單元。)我們使用證書作為標(biāo)簽,然后這#n= 來標(biāo)記一個對象,用#n#來在表達式中表示對象位置。
#1=(A B C . #1#)
練習(xí)題
2.36 反駁:列表不能只用CONS函數(shù)來構(gòu)造。提示:想想單元被構(gòu)造起來的順序。
更加奇怪的是下面這一個,這個內(nèi)存單元的CAR是指向自己的。

如果計算機想要打印這個結(jié)構(gòu),那么將會技術(shù)在一個左括號循環(huán)中。但是如果是命令打印井號等位表達式:
#1=(#1# . A)
2.19 非列表內(nèi)存結(jié)構(gòu)的長度
列表的長度是頂層層次的內(nèi)存單元個數(shù),但是列表(A B C . D)的長度是3,而不是4,同樣長度的列表是(A B C),同樣也可以寫成(A B C . NIL)。

如果給出一個循環(huán)列表作為輸入,例如#1=(A B C . #1#),LENGTH函數(shù)可能返回一個值,在大部分視線中是會陷入無限循環(huán)。