Common Lisp:符號計算簡單介紹(第六章)

第六章 列表數據結構

6.1 導語

本章會展示更多列表處理函數和列表如何被應用在其他數據結構中,比如集合,表格和樹。Common Lisp相比于其他語言的強處之一就是,提供了很多支持這些數據結構的內建函數。Lisp程序員就哭了一很快的聚焦在他想解決的問題上。Pascal或者C程序員在解決實際問題之前必須先跳出來,先去實現一個類似于Lisp提供的系統,例如連接列表的原語,字符數據結構,存儲的分配等等。
在本章我們會比第二章的時候處理列表更加復雜一些。不止會塔倫一些列表處理函數的行為,而且還會看到他們內部的工作。我們先預先準備一下,復習2.17小節學習到的點對表達式。如果你還沒有閱讀先前的進階話題單元,現在去讀讀吧。

6.2 括號表達式VS內存單元表達式

用括號表達式來表示列表是很方便的,但是也會產生誤導。使用括號表達式表示的列表是對稱的:由一個左括號開始并結束于一個右括號。因此,cons函數處理參數也是對稱的,cons函數在一個列表前加上一個元素,如下:

>(cons ’w ’(x y z)) → (w x y z)

為什么我們不能在一個列表的尾部加上一個元素呢?初學者這樣嘗試之后會被結果驚呆了:

>(cons ’(a b c) ’d) → ((a b c) . d)

如果我們在括號表達式中,訪問一個列表的左端和右端的區別沒有理由這么巨大。但是切換到內存單元表達式來看,區別就非常巨大了。列表是由指針構成的單向鏈條。對于列表來說我們很容易在列表前面添加一個項目是因為,我們實際上做的是創造一個新的單元并且白cdr指向現有的列表,只是這樣。如果將W和(X Y Z)輸入cons函數,結果就是一個新的內存單元,它的car指向w,cdr指向原有的列表,如下所示。雖然我們把結果寫成這樣(W X Y Z),但是實際上也可以用點對表達式來表示(W . (X Y Z))。



列表(A B C)和D輸入cons函數的時候,新內存單元的car是指向原有列表(A B C),cdr是指向符號D。結果就是((A B C) . D),用括號表達式看上去是很奇怪。這個點的存在是絕對必須的,因為這個列表是由其他元符號而不是nil來結尾。用內存單元表達式看起來是這樣:



由于原先的列表cdr已經指向了nil,所以通過新建內存單元的的方法來給列表尾部加上一個元素的方法是不可行的。更加復雜的技術將會被引用,其中一個方法在下一個小節會被介紹。

6.3 append函數

append函數接受兩個列表作為輸入,返回一個包括所有元素的列表,第一個列表的元素之后是第二個列表的元素。

> (append ’(friends romans) ’(and countrymen))
(FRIENDS ROMANS AND COUNTRYMEN)
> (append ’(l m n o) ’(p q r))
(L M N O P Q R)

如果append函數的輸入列表是空列表,那么結果就等于其他輸入,給一個列表追加空列表就相當于在一個數字上加上0。

> (append ’(april showers) nil)
(APRIL SHOWERS)
> (append nil ’(bring may flowers))
(BRING MAY FLOWERS)
> (append nil nil)
NIL

append函數對于嵌套列表也是起作用的,他只著眼于頂層括號,所以不必在意輸入是不是嵌套列表。

> (append ’((a 1) (b 2)) ’((c 3) (d 4)))
((A 1) (B 2) (C 3) (D 4))

append函數不會改變任何變量或者現有內存單元的值,所以也被稱為非破壞性函數。

> (setf who ’(only the good))
(ONLY THE GOOD)
> (append who ’(die young))
(ONLY THE GOOD DIE YOUNG)
> who
(ONLY THE GOOD) The value of WHO is unchanged.
(吐槽:作者這是滿滿的惡意么?好人死得早?搜索一下才發現是一首77年老歌的名字)

append函數的兩個輸入看上去好像是對稱的,但這只是括號表達式帶來的一個假象。append函數對待兩個輸入是完全不同的。當往列表(D E)后面追加列表(A B C)的時候,append賦值第一個輸入但是不復制第二個。第一個輸入的最后一個內存單元的cdr指向了第二個輸入,返回一個復制的列表的指針。如圖6-1所示。
對append函數的工作原理描述過后,就解釋了為什么在第一個輸入不是列表的時候會報錯,但是第二個輸入不是列表的時候就ok。

>(append ’a ’(b c d)) → Error! A is not a list.
>(append ’(w x y) ’z) → (W X Y . Z)

append想要復制第一個輸入的內存單元,但由于第一個輸入A并不是一個列表,所以報錯了。但是當往列表(W X Y)上追加Z的時候,append可以復制他的第一個輸入然后最后一個內存單元的cdr會指向第二個輸入,所以不會報錯。因為第二個輸入不是一個列表,所以結果看上去還是有點奇怪。
現在我們需要解決的問題就是,把一個元素加載一個列表的后面,如果我們先把元素做成一個列表,那么就可以使用append來解決這個問題了。

>(append ’(a b c) ’(d)) → (A B C D)
(defun add-to-end (x e)
"Adds element E to the end of list X."
(append x (list e)))
(add-to-end ’(a b c) ’d) → (A B C D)

比較cons,list和append

一開始Lisp程序員會在cons,list和append函數之間的區分上有所困惑,因為這三個函數都有構造列表數據結構的功能。這里是一個對三個函數的簡單回顧:

  • cons函數創造一個新的內存單元,經常被使用在列表前面需要增加一個元素的時候。
  • list函數通過接受任意數量的輸入來創造一個新的列表,而且在最后一個元素的內存單元以nil結束。每一個元素的car指向對應的輸入。
  • append函數將列表合成在一起。通過復制第一個輸入,然后使第一個輸入的最后一個單元的cdr指向第二個輸入。如果第一個輸入不是列表的話將會出現錯誤。

現在我們拉進一步比較他們。首先考慮一種,第一個輸入是符號,第二個輸入是列表的情況:

> (cons ’rice ’(and beans))
(RICE AND BEANS)
> (list ’rice ’(and beans))
(RICE (AND BEANS))
> (append ’rice ’(and beans))
Error: RICE is not a list.

然后我們看看兩個輸入都是列表的情況:

> (cons ’(here today) ’(gone tomorrow))
((HERE TODAY) GONE TOMORROW)
> (list ’(here today) ’(gone tomorrow))
((HERE TODAY) (GONE TOMORROW))
> (append ’(here today) ’(gone tomorrow))
(HERE TODAY GONE TOMORROW)

最后我們來看看第一個輸入是列表,第二個輸入是符號的情況。這是最讓人困惑的情況,你必須用內存單元表達式來再想想。

> (cons ’(eat at) ’joes)
((EAT AT) . JOES)
> (list ’(eat at) ’joes)
((EAT AT) JOES)
> (append ’(eat at) ’joes)
(EAT AT . JOES)

為了更多的而開發你對這三個函數的直覺,嘗試一下使用sdraw工具來驗證上面的例子,lisp toolkit中的sdraw工具是用來畫內存單元表達式圖的。

6.5 更多關于列表的函數

lisp提供了很多用來操作列表的簡單函數,我們已經討論過的有cons,list,append和length。他們之中有些必須復制它的輸入,有些則不需要。接下來請看這樣設置的理由。

6.5.1 reverse(反轉)

reverse函數提供一個列表的反轉。

> (reverse ’(one two three four five))
(FIVE FOUR THREE TWO ONE)
> (reverse ’(l i v e))
(E V I L)
> (reverse ’live)
Error: Wrong type input.
> (reverse ’((my oversight)
(your blunder)
(his negligence)))
((HIS NEGLIGENCE) (YOUR BLUNDER) (MY OVERSIGHT))

請注意reverse函數只是反轉頂層括號的元素。嵌套列表里的作為元素的列表不會被操作。關于reverse函數的另一點就是他不支持符號的操作。列表(L I V E)的反轉是列表(E V I L),但是符號live的反轉會返回一個類型輸入錯誤。
就像append一樣,reverse函數也是非破壞性的。他會復制輸入而不是改變輸入。

> (setf vow ’(to have and to hold))
(TO HAVE AND TO HOLD)
(reverse vow)
(HOLD TO AND HAVE TO)
vow
(TO HAVE AND TO HOLD)

我們可以像下面一樣使用reverse函數來給一個列表的最后加上一個元素。假設,我們想要給列表(A B C)加上元素D。列表(A B C)的反轉就是(C B A)。把符號D加到這個反轉列表的前面,再一次反轉,就得到了列表(A B C D)。

(defun add-to-end (x y)
(reverse (cons y (reverse x))))
(add-to-end ’(a b c) ’d) → (a b c d)

現在你知道了兩種在列表后面加上元素的方案。append方案和雙reverse方案,相比之下,append方案會更好一些,因為雙reverse方案會進行兩次復制,append方案會更有效率一些。在本章的結尾,我們會在進階話題中進一步討論效率問題。

6.5.2 nth和nthcdr

nthcdr函數返回的是列表的連續第n個的cdr。當然了我們輸入0個的話,就會返回原來的列表,如果要的個數多過列表的長度,就會返回NIL。

(nthcdr 0 ’(a b c)) → (a b c)
(nthcdr 1 ’(a b c)) → (b c)
(nthcdr 2 ’(a b c)) → (c)
(nthcdr 3 ’(a b c)) → nil

使用大于3的數字輸入并不會引發錯誤;我們會得到和輸入3相同的結果。的腳的結論之一就是nil的cdr就是nil。

(nthcdr 4 ’(a b c)) → nil
(nthcdr 5 ’(a b c)) → nil

但如果列表的結尾不是nil而是一個元字符的話,輸入的數字超過長度就會引發錯誤。

(nthcdr 2 ’(a b c . d)) → (c . d)
(nthcdr 3 ’(a b c . d)) → d
(nthcdr 4 ’(a b c . d)) → Error! D is not a list

函數nth是返回一個列表的第n個元素。

(defun nth (n x)
"Returns the Nth element of the list X,
counting from 0."
(car (nthcdr n x)))

既然(NTHCDR 0 x)得到的是列表,(NTH 0 x)得到的是第一個元素,以此類推,(NTH 1 x)就是得到第二個元素。

(nth 0 ’(a b c)) → a
(nth 1 ’(a b c)) → b
(nth 2 ’(a b c)) → c
(nth 3 ’(a b c)) → nil

數數字的時候是從0開始而不是從1開始是Common Lisp一直遵循的慣例。在我們討論數組的時候會在此接觸到這一點。

6.5.3 last函數

last函數返回的是一個列表的最后一個內存單元,這個內存單元的car是列表的最后一個元素。根據定義,這個單元的cdr是一個元字符。否則就不是列表的最后一個單元了。如果列表為空,last會返回nil。

(last ’(all is forgiven)) → (forgiven)
(last nil) → nil
(last ’(a b c . d)) → (c . d)
(last ’nevermore) → Error! NEVERMORE is not a list.

6.5.4 remove函數

remove函數會刪除列表中的一個項目。正常情況下回刪除所有適合條件的元素,也是有方法刪除其中一部分的(在本章進階話題中)。remove返回的結果是一個刪除了相關元素的新的列表。

(remove ’a ’(b a n a n a)) → (b n n)
(remove 1 ’(3 1 4 1 5 9)) → (3 4 5 9)

remove函數是一個非破壞性函數,當從列表中刪除變量的時候,他不會改變任何變量和內存單元。remove得到的結果是一個原來列表的部分的拷貝。

> (setf spell ’(a b r a c a d a b r a))
(A B R A C A D A B R A)
> (remove ’a spell)
(B R C D B R)
> spell
(A B R A C A D A B R A)

下面的表格會幫助你記憶,那些函數拷貝輸入進行處理,哪些不拷貝。append,reverse,remove返回一個新的不包括輸入的內存單元鏈,所以他們是要拷貝他們的輸入的。像nthcdr,nth和last函數會返回一個指針指向輸入的部分組件。他們不需要拷貝任何對象,因為他們所需要的對象都已經存在了。


6.6 列表構成集合

集合是對象的無序集合。每一個對象只在該集合中出現一次。一些典型的集合就是,一個星期的天數,整數的集合(一個無限集合),還有住在王家屯晚上吃了麻辣燙的集合。
集合毫無疑問是有列表組成的一個更加有用的數據結構。一些基本的集合操作比如說測試一個元素是不是在這個集合當中,獲取兩個集合的并集,交集,差集(也叫做集合減法),還有測試一個集合是不是另一個集合的子集合。這些所有操作的Lisp函數都會在下面介紹。

6.6.1 成員(member)

member斷言的作用是檢查一個元素是不是列表的成員。如果是列表的成員,那么有這個元素開始的字列表將會被返回,不是的話,會返回nil。member絕不會返回T,但是傳統意義上這是一個斷言,因為如果元素術語這個列表就會返回非NIL。

> (setf ducks ’(huey dewey louie)) Create a set of ducks.
(HUEY DEWEY LOUIE)
> (member ’huey ducks) Is Huey a duck?
(HUEY DEWEY LOUIE) Non-NIL result: yes.
> (member ’dewey ducks) Is Dewey a duck?
(DEWEY LOUIE) Non-NIL result: yes.
> (member ’louie ducks) Is Louie a duck?
(LOUIE) Non-NIL result: yes.
> (member ’mickey ducks) Is Mickey a duck?
NIL NIL: no

在Lisp的第一個方言中,member函數值返回T或者NIL。但是熱么決定使得member函數返回這個元素開始子列表,成為了一個非常有用的功能。這個擴展一如既往的伴隨member作為一個斷言,因為沒有元素的子列表就是false。
這里有一個例子說明為什么member返回子列表是非常有用的。斷言beforep,在列表l中,x比y先出現的話,返回真。

(defun beforep (x y l)
"Returns true if X appears before Y in L"
(member y (member x l)))
> (beforep ’not ’whom
’(ask not for whom the bell tolls))
(WHOM THE BELL TOLLS)
> (beforep ’thee ’tolls ’(it tolls for thee))
NIL

6.6.2 交集(intersection)

intersection函數是去獲取兩個列表的交集返回一個兩個列表的共有元素的列表。在結果中,元素出現的順序是未定義的,可能每一個Lisp實現都不相同。另外在集合中順序是不重要的,只有元素本身是重要的。

> (intersection ’(fred john mary)
’(sue mary fred))
(FRED MARY)
> (intersection ’(a s d f g)
’(v w s r a))
(A S)
> (intersection ’(foo bar baz)
’(xam gorp bletch))
NIL

如果一個列表中出現了多個一樣的元素,那就不是一個真的集合。Common Lisp的集合函數,比如intersection和union都可以處理不是集合的列表,但是結果是不是包括重復元素就沒有準確定義了,們多事情可能還是要看具體實現。

6.6.3 并集(union)

union函數返回兩個集合的并集,換言之,由兩個列表的所有元素組成的不重復列表,一個元素在兩個列表中都出現了的話,那么在結果中就只存在一份。對集合來說,結果的元素出現順序并不重要(也沒有定義)。

> (union ’(finger hand arm)
’(toe finger foot leg))
(FINGER HAND ARM TOE FOOT LEG)
> (union ’(fred john mary)
’(sue mary fred))
(FRED JOHN MARY SUE)
> (union ’(a s d f g)
’(v w s r a))
(A S D F G V W R)

6.6.4 差集(set-difference)

set-difference函數的功能室差集。返回的是第一個集合去除和第二個集合重復的元素的結果。再次說明,元素出現的順序沒有定義。

> (set-difference ’(alpha bravo charlie delta)
’(bravo charlie))
(ALPHA DELTA)
> (set-difference ’(alpha bravo charlie delta)
’(echo alpha foxtrot))
(BRAVO CHARLIE DELTA)
> (set-difference ’(alpha bravo) ’(bravo alpha))
NIL

不像union和intersection,set-difference函數不是一個對稱函數。調換第一個和第二和輸入會導致結果的不同。

(setf line1 ’(all things in moderation))
(setf line2 ’(moderation in the defense of liberty
is no virtue))
> (set-difference line1 line2)
(ALL THINGS)
> (set-difference line2 line1)
(THE DEFENSE OF LIBERTY IS NO VIRTUE)

6.6.5 substep斷言

如果一個集合包括了另一個,斷言substep返回T,換句話說,第一個集合每一個元素都是第二個集合的元素。

(subsetp ’(a i) ’(a e i o u)) → t
(subsetp ’(a x) ’(a e i o u)) → nil

6.7 用集合編程

這里的一個例子是說如何使用集合解決一個編程問題。這個問題是給一個名字的前面加上一個稱呼,吧john doe變成mr john doe或者吧jane doe變成ms jane doe。如果一個名字已經有稱呼了,那么標題應該被保留,但是如果沒有的話,我們來嘗試根據第一個名字來決定性別然后加上合適的稱呼。
為了解決這樣的問題,我們必須把他分解成小問題,讓我們開始在這個首先名字前面有沒有稱呼這個小問題上。這里有定義一個函數來解決這個問題。

(defun titledp (name)
(member (first name) ’(mr ms miss mrs)))
> (titledp ’(jane doe)) ‘‘Jane’’ is not a title.
NIL
> (titledp ’(ms jane doe)) ‘‘Ms.’’ is in the set of titles.
(MS MISS MRS)

下一個步驟就是找出這個單詞是一個男性名字還是女性名字。我們會使用簡化一些的樣本來是的我們的例子保持簡潔。

(setf male-first-names
’(john kim richard fred george))
(setf female-first-names
’(jane mary wanda barbara kim))
(defun malep (name)
(and (member name male-first-names)
(not (member name female-first-names))))
(defun femalep (name)
(and (member name female-first-names)
(not (member name male-first-names))))
> (malep ’richard) ‘‘Richard’’ is in the set of males.
T
> (malep ’barbara) ‘‘Barbara’’ is not a male name.
NIL
> (femalep ’barbara) ‘‘Barbara’’ is a female name.
T
> (malep ’kim) ‘‘Kim’’ can be either male or female,
NIL so it’s not exclusively male.

現在我們可以寫一個give-title函數來給名字加上稱呼。當然,我們只給沒有稱呼的名字加上去。如果名字既不屬于男性,也不屬于女性那就加上mr or ms。

(defun give-title (name)
"Returns a name with an appropriate title on
the front."
(cond ((titledp name) name)
((malep (first name)) (cons ’mr name))
((femalep (first name)) (cons ’ms name))
(t (append ’(mr or ms) name))))
> (give-title ’(miss jane adams)) Already has a title.
(MISS JANE ADAMS)
> (give-title ’(john q public)) Untitled male name.
(MR JOHN Q PUBLIC)
> (give-title ’(barbara smith)) Untitled female name.
(MS BARBARA SMITH)
> (give-title ’(kim johnson)) Untitled, and gender
(MR OR MS KIM JOHNSON) is ambiguous

在這個例子中比較重要的特性有,1,將問題分解為小函數。2,逐一測試寫的每一個函數,一旦寫好了titlep,malep和femaiep等斷言,give-title函數就很好寫了。
講一個問題分解為子問題是一個很重要的技能。有經驗的程序員京城可以看到正確的方式如何講一個問題分解成為邏輯上的子問題,但是初學者必須同過聯系來建立這種直覺。
Here are a few more things we can do with these lists of names. The
functions below take no inputs, so their argument list is NIL.
還有一件我們可以做的事情就是關于這些列表的名字。下面的函數沒有輸入,也就是參數列表是NIL。

(defun gender-ambiguous-names ()
(intersection male-names female-names))
(gender-ambiguous-names) → (kim)
(defun uniquely-male-names ()
(set-difference male-names female-names))
(uniquely-male-names) → (john richard fred george)

到現在為止,我們在本章見過的所有集合都只是由字符或者數字組成。操作列表組成的集合也是很簡單的,但是使用像member,union,intersection等等函數的話需要一個小技巧。詳情請見進階話題的討論。

6.8 列表組成的表格

表格(tables)是另一個我們可以由列表構建的非常有用的數據結構。一個表格,或者聯合列表,是一個列表的列表。每一個列表被稱作一個入口(entry),每一個入口的car就是入口的key、下面顯示的就是一個五個英文單詞和他們的法語翻譯組成的表格。這個二表哥包括5個入口,key就是英語單詞。

(setf words
’((one un)
(two deux)
(three trois)
(four quatre)
(five cinq)))

6.8.1 assoc函數

The ASSOC function looks up an entry in a table, given its key. Here are
some examples.
assoc函數在表格中給出一個key,找到一個入口,下面是例子:

(assoc ’three words) → (three trois)
(assoc ’four words) → (four quatre)
(assoc ’six words) → nil

assoc尋找五個之中匹配的入口,然后返回整個入口。如果assoc沒有找到入口,就返回nil。
Notice that when ASSOC does find an entry with the given key, the value
it returns is the entire entry. If we want only the French word and not the
entire entry, we can take the second element of the result of ASSOC.
請注意當assoc找到對應key的入口的時候,返回的值是整個入口。如果我們只想要法語翻譯的話那么就對assoc的結果進行進一步處理,得到入口第二個元素。

(defun translate (x)
(second (assoc x words)))
(translate ’one) → un
(translate ’five) → cinq
(translate ’six) → nil

6.8.2 rasscoc

rassoc類似于assoc,但是是著眼于每一個表格的元素的cdr而不是car。(他的名字就是Reverse assoc的縮寫)。為了使用符號作為key的rassoc,表格必須使一個點對的列表:

(setf sounds
’((cow . moo)
(pig . oink)
(cat . meow)
(dog . woof)
(bird . tweet)))
(rassoc ’woof sounds) ? (dog . woof)
(assoc ’dog sounds) ? (dog . woof)

assoc和rassoc都是返回他們找到的第一個符合key的入口,身下的列表就不被搜索了。

6.9 表格編程

這里有assoc的另一個例子。我們將要創造一個對象以及他們描述的表格,這個表格會存儲在全局變量things中:

((object1 large green shiny cube)
(object2 small red dull metal cube)
(object3 red small dull plastic cube)
(object4 small dull blue metal cube)
(object5 small shiny red four-sided pyramid)
(object6 large shiny green sphere))

現在我們來開發函數來告訴我們,在那個對象中有兩個對象不同。我們一開始來寫一個叫做description的函數來導出一個對象的描述。

(defun description (x)
(rest (assoc x things)))
(description ’object3) → (red small dull plastic cube)

兩個對象之間的區別是有什么屬性出現在了第一個對象的描述中卻沒有出現在第二個當中,或者在第二個對象的描述中出現但第一個沒有。用技術術語來說就是異或。這是Common Lisp的內建函數。

(defun differences (x y)
(set-exclusive-or (description x)
(description y)))
(differences ’object2 ’object3) → (metal plastic)

object2是金屬,但是object3是塑料,所以金屬和塑料的屬性是完全不同的。我們可以根據他們指向的類型的不停來分辨屬性。這里是一個點對列表的表格:

(setf quality-table
’((large . size)
(small . size)
(red . color)
(green . color)
(blue . color)
(shiny . luster)
(dull . luster)
(metal . material)
(plastic . material)
(cube . shape)
(sphere . shape)
(pyramid . shape)
(four-sided . shape)))

我們可以使用這個表格作為函數的一部分來給我們指出給定屬性的物質:

(defun quality (x)
(cdr (assoc x quality-table)))
(quality ’red) → color
(quality ’large) → size

Using DIFFERENCES and QUALITY, we can write a function to tell us
one quality that is different between a pair of objects.
使用differences和quality,我們可以寫一個函數來告訴我們一個物質在兩個對象之間有什么不同。

(defun quality-difference (x y)
(quality (first (differences x y))))
(quality-difference ’object2 ’object3) → material
(quality-difference ’object1 ’object6) → shape
(quality-difference ’object2 ’object4) → color

如果我想要一個所有物質區別的列表而不只是一個怎么辦?我們需要一個方法來從區別的列表(RED
BLUE METAL PLASTIC) 到對應物質的列表轉換。然后我們也不得不評價多個元素。第一部分可以依靠sublis完成,這個函數我們將在進階話題中討論。

(differences ’object3 ’object4) → (red blue metal plastic)
> (sublis quality-table
(differences ’object3 ’object4))
(COLOR COLOR MATERIAL MATERIAL)

現在我們不得不做的是在結果中評價多個入口。Common Lisp提供了一個函數佳作remove-duplicates來達到這個目的。

(defun contrast (x y)
(remove-duplicates
(sublis quality-table (differences x y))))
(contrast ’object3 ’object4) → (color material)

小結

列表在自己應用范圍是一種重要的數據結構。但是在lisp中他更重要的地方在于他能夠用來構成其他數據結構,集合和表格。
如所見,解決任何不簡單的問題的方法及時將問題分解成小問題,一個個可控的片段。一些簡單的函數可以一個個測試并組合成主要問題的解決方案。

本章涉及函數

列表函數: APPEND, REVERSE, NTH, NTHCDR, LAST, REMOVE.
集合函數: UNION, INTERSECTION, SET-DIFFERENCE, SETEXCLUSIVE-OR, MEMBER, SUBSETP, REMOVE-DUPLICATES.
表格函數: ASSOC, RASSOC

Lisp Toolkit:sdraw

sdarw是一個用來畫列表對應的內存單元表達式的工具。他并不是Common Lisp標準中的一部分;他被定義在附錄A當中。在完整的移植版本中會運行任何Common Lisp實現,使用普通字符來畫出內存單元表示:



還有一個圖形版本,可以從發行商那里的軟盤獲得,是使用圖形來表示內存單元和箭頭的函數。那個圖形表示看上去更好看一些。一個圖形版本是使用CLX,Common Lisp對應X Window System的接口。如果你的電腦不是運行Xwindow的話,會不能使用這個版本。但是要是你的Lisp支持其他圖形實例,也會很容易使用sdraw。
另一個有用的工具像函數sdraw-loop,就像一個read-eval-print循環一樣工作的畫圖函數。sdraw-loop的提示符就是字符串S>。


第六章進階話題

6.10 樹結構(trees)

樹是一個嵌套列表結構。到現在為止所有的函數都只是對于列表的頂層進行操作,他們并不進行更深入結構的操作。Lisp也包括一些新的函數來操作整個列表。其中的兩個就是subst和sublis。在第八章中我們會寫很多這樣操作樹結構的函數。

6.10.1 subst

subst函數經列表中的一個項目替換(substitute)為另一個項目,不論這個元素出現在列表的何處。這是一個三輸入的函數,輸入的順序是,用x替換z中的y。

> (subst ’fred ’bill
’(bill jones sent me an itemized
bill for the tires))
(FRED JONES SENT ME AN ITEMIZED
FRED FOR THE TIRES)

如果要被替換的字符沒有出現在該列表中,則返回原來的列表。

> (subst ’bill ’fred ’(keep off the grass))
(KEEP OFF THE GRASS)
> (subst ’on ’off ’(keep off the grass))
(KEEP ON THE GRASS)

subst著眼于整個列表結構,不僅僅是頂層元素。

> (subst ’the ’a
’((a hatter) (a hare) and (a dormouse)))
((THE HATTER) (THE HARE) AND (THE DORMOUSE))

6.10.2 sublis

sublis類似于subst,除了他可以同時替換多個元素。第一個輸入是一個點對作為入口形成的表格。第二個輸入是將要做出替換的列表。

> (sublis ’((roses . violets) (red . blue))
’(roses are red))
(VIOLETS ARE BLUE)
(setf dotted-words
’((one . un)
(two . deux)
(three . trois)
(four . quatre)
(five . cinq)))
> (sublis dotted-words ’(three one four one five))
(TROIS UN QUATRE UN CINQ)

6.11 列表操作的效率

在本章的一開始我們討論了在括號表達式中,列表如何表現出對稱性,但是實際上不是這樣的。另一個方法就是在相對速度或者制定操作的效率上表現出的非對稱性。例如,提取一個列表的首個元素很容易,但是提取最后一個元素卻要很花力氣。當要提取起一個元素的時候,我們從指向第一個內存單元的指針開始。函數first僅僅是提取了那個單元的car然后返回他。想要找到列表的最后一個元素要做的工作就要多得多。因為唯一的方法就是一直找啊找知道遇到一個元素的cdr是一個元字符的時候,我們才可以看到那個單元的car。如果原來的列表很長,那么可能要好一會才能找到最后一個單元。
計算機可以循序成百上千的單元來找到想要的單元,時間還非常短,所以一般不需要注意到first和last在速度上的差別。但是如果軟件項目的規模變得很大,那么這個細微差別就會變得引人注意。
另一個會影響函數速度的事實就是有多少切實的單元存在。創造一個新的單元是要耗費時間的,最后也會占滿計算機的內存。在事實上有一些會被棄用,但是所占的內存卻還在。在一些Lisp實現中,內存會被沒有用的單元給占滿,機器必須暫時停下來然后運行垃圾回收機制。一個函數要處理的單元越多,垃圾回收機制運行的就越頻繁。我們接下來來比較一下這兩個版本add-to-and函數的效率:

(defun add-to-end-1 (x y)
(append x (list y)))
(defun add-to-end-2 (x y)
(reverse (cons y (reverse x))))

假設這些函數的輸入是一個n元素的列表。add-to-end1使用append來拷貝第一個輸入,然后將第二個輸入定位到最后。因此他會創造一個完整的n+1列表。add-to-end2開始是將列表反轉,這將會創造一個新的列表,然后將第二個輸入加載這個列表的前面,最后反轉結果,再次創造一個新的列表。所以add-toend2創造的是n+1+n+1個單元。其中n+1是結果,其他的n+1將會在創造后被拋棄,成為垃圾,很銘心啊add-to-end1是一個更加有效率的函數,因為創造了更少的單元。

6.12 共享結構

當兩個列表共享內存單元的時候,就被稱為共享結構。在顯示器上輸出的列表是不可能體現出共享結構的,因為每一個列表看上去都是一個新列表。通過使用car,cdr和cons可以構造全新的共享列表。例如我們使得兩個列表共享一些元素。

> (setf x ’(a b c))
(A B C)
> (setf y (cons ’d (cdr x)))
(D B C)

列表X的值是(A B C),Y的值是(D B C)。兩個列表共享了一部分內存單元(B C)。共享結構的建立是因為Y是由(CDR X)而來的。如果我們直接簡單定義Y,就不會有共享結構。


6.13 對象的相等

在Lisp中,符號是唯一的,意味著他們在計算機內存中只能是一個具名符號。每一個內存中的對象命名位置,我們叫他們地址(address)。所以在列表(TIME AFTER TIME)中。是沒有兩個字符叫做time的。



列表,從另一個角度說也不是唯一的。我們可以很簡單的用獨立的內存單元鏈條來構造不同的列表(A B C),兩個列表中的字符是唯一的,但是列表本身不是唯一的。這意味著equal函數是不可以通過比較兩者的地址來知道他們是不是相等,因為兩個(A B C)是相等的但是他們是不同的內存單元。equal函數因此事一個一個元素的比較兩個列表。如果兩個列表的對應元素是相等的,那么列表本身就被認為是相等的。

> (setf x1 (list ’a ’b ’c)) Make a fresh list (A B C).
(A B C)
> (setf x2 (list ’a ’b ’c)) Make another list (A B C).
(A B C)
> (equal x1 x2) The lists are EQUAL.
T

如果我們想知道兩個指針是不是指向相同的對象,就必須比較他們的地址。EQ斷言就是做這個事情的。列表之間如果有相同的地址,那么他們就是相等的(EQ),不會一個個比較元素。

> (eq x1 x2) The two lists are not EQ.
NIL
> (setf z x1) Now Z points to the same list as X1.
(A B C)
> (eq z x1) So Z and X1 are EQ.
T
> (eq z ’(a b c)) These lists have different addresses.
NIL
> (equal z ’(a b c)) But they have the same elements.
T

EQ函數要比eqaul快,因為eq只是比較一個個地址,equal首先要測試輸入是不是列表,如果是必須每一個元素相對應的比較。由于更有效率,程序員更喜歡使用eq而不是equal,除非是想要知道內存單元是不是相同。
數字,在不同的Lisp系統中也有不同的展現。在一些實現中,每一個數字都有一個唯一的地址,然而在另一些實現中不是。因此eq不應該用來比較數字。
eql斷言的行為大體上跟eq相同。他跟eql一樣比較兩個對象的地址,除開對于兩個相同類型(例如都是整形)數字的情況下。他會比較兩個數字的值。不同類型的數字在eql中不相等,即使是值相等。

(eql ’foo ’foo) → t
(eql 3 3) → t
(eql 3 3.0) → nil Different types.

eql是Common Lisp中的“默認”比較斷言。例如member函數和assoc函數都是默認使用eql作為相等的比較函數,除非指定使用其他的。
對于比較兩個完全不同類型的數字,現在也有另一個比較斷言叫做=。這個斷言是最常用的比較連個數字的方法。給出數字外的其他輸入會報錯。

(= 3 3.0) → t
(= ’foo ’foo) → Error! FOO is not a number

最后,equalp斷言是相似于equal的斷言,但是在一些情況下更加寬容一些。一個例子就是他會忽視字符串比較的大小寫。

(equal "foo bar" "Foo BAR") → nil
(equalp "foo bar" "Foo BAR") → t

初學者經常會對Common Lisp中的大量相等比較感到困惑,我推薦的是忘記這些所有特殊的函數。只是記住兩條建議。第一,使用equal,他就是你想要的。第二內建函數memeber和assoc等等在內部出于效率都是使用eql作為默認的。這意味著他們頸部會正確比較列表,除非你告訴他們用不同的相等斷言。下一小節我們解釋如何做到這一點:
概述一下:

  • eq是最快的相等測試,他比較的是地址。專家一般是用這個來快速比較符號,測試他們是不是在內存單元中是同一個物理單元。eq不應該被用來比較數字。
  • eql類似于eq,除了他可以安全的比較相同類型的數字,例如兩個整形或者兩個浮點數。在common Lisp中他是默認的相等測試斷言。
  • equal是一個初學者使用的斷言。他逐個比較列表的元素,除此之外和eql的行為相同。
  • equalp是更加寬容的斷言,和equal比較的話,他會忽視字符大小寫的諸如此類的問題。
  • =是一個比較數字最有效的方式,也是比較不同類型數字的唯一方式,比如3和3.0.他只接受數字。

6.14 關鍵字參數(keyword argument)

很多Common Lisp函數都支持在列表中附加的,可選的參數,叫做關鍵字參數。例如,remove函數可以接受一個可選的參數叫做,:count來告訴函數,到底刪除多少個對象。

(setf text ’(b a n a n a - p a n d a))
> (remove ’a text) Remove all As.
(B N N - P N D)
> (remove ’a text :count 3) Remove 3 As.
(B N N - P A N D A)

remove也接受一個:from-end關鍵字。如果他的值不是nil,那么remove就會從列表的最后開始操作,而不是從開始。所以為了刪除列表中的最后兩個A,我們可以這樣寫:

> (remove ’a text :count 2 :from-end t)
(B A N A N A - P N D)

一個關鍵字是一個特殊類型的字符,他的名字總是在一個冒號的后面。字符count和:count不是相同的,他們是不同的對象,在eq中也是不同的。關鍵字總是求值為自身的,所以他們不需要被引用,嘗試改變一個關鍵字的值會引發錯誤。斷言keywordp,如果輸入是關鍵字的話就會返回T。

:count → :count
(symbolp :count) → t
(equal :count ’count) → nil
(keywordp ’count) → nil
(keywordp :count) → t

另一個可以接受關鍵字參數的函數是member,正常來說,member使用eql來測試一個項目是不是出現在了一個集合里。eql對符號和數字都是有效的。但是假設我們的集合包括了列表怎么辦?在那種情況下我們必須使用equal來做相等判斷,否則其他的member將不會找到我們要找的項目。

(setf cards
’((3 clubs) (5 diamonds) (ace spades)))
(member ’(5 diamonds) cards) → nil
(second cards) → (five diamonds)
(eql (second cards) ’(5 diamonds)) → nil
(equal (second cards) ’(5 diamonds)) → t

關鍵字:test可以再member函數中使用來指定一個相等判斷的函數。我們在特定的引號后面寫上函數名作為一種輸入。

> (member ’(5 diamonds) cards :test #’equal)
((5 DIAMONDS) (ACE SPADES))

所有的列表函數,包括相等測試等等都接受一個:test關鍵字參數。remove函數是另一個例子,我們不能從cards中刪除(5 diamonds)除非我們告訴remove使用equal作為相等判斷。

> (remove ’(5 diamonds) cards)
((3 CLUBS) (5 DIAMONDS) (ACE SPADES))
> (remove ’(5 diamonds) cards :test #’equal)
((3 CLUBS) (ACE SPADES))

另一個接受:test關鍵字的函數是union,intersection和set-difference,assoc,rassoc,subst和sublis。為了找出函數接受哪一個關鍵字,可以使用在線文檔。給函數輸入他不支持的關鍵字的話會報錯。

> (remove ’(ace spades) cards :reason ’bad-luck)
Error! :REASON is an invalid keyword argument
to REMOVE.
進階話題涉及函數

樹函數: SUBST, SUBLIS.
附加的相等函數: EQ, EQL, EQUALP, =.
關鍵字斷言: KEYWORDP

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容