AI編程范式 第3章 Lisp概覽(下)

3.4 說說相等和內(nèi)部表示

在Lisp中主要有5種相等斷言,因為不是所有的對象被創(chuàng)建的時候都是相等意義上的相等。數(shù)字相等斷言,=,測試兩個數(shù)字是不是一樣。當(dāng)把=用在非數(shù)字上的時候,就會出錯。其他的等于斷言可以操作任何對象,但是要理解他們之間的區(qū)別,我們需要理解Lisp的內(nèi)部表達(dá)方式。
當(dāng)Lisp在不同的地方讀取一個符號的時候,他的結(jié)果肯定是一樣的符號。Lisp系統(tǒng)會會務(wù)一張符號表,提供read函數(shù)使用來定位字符和符號之間的映射。但是在不同的地方讀取或者構(gòu)建一個列表的時候,結(jié)果并不是完全意義上的相同,甚至對應(yīng)的元素也是。這是因為read調(diào)用cons來構(gòu)建列表,每一次對cons的調(diào)用都會返回一個新的組合單元。下圖展示了兩個列表x和y,他們都等于(one two),但是他們是由不同的組合單元組成的,因此并不完全相同。下面的圖片還展示了表達(dá)哈斯(rest x)并不會產(chǎn)生新的組合單元,而是和原來的結(jié)構(gòu)共享x,所以表達(dá)式(cons ‘zero x)會生成一個新的組合單元,它的rest就是x。


兩個列表的相等性判斷.JPG

當(dāng)兩個數(shù)學(xué)意義上相等的數(shù)字被讀取或者被計算的時候,他們可能相等,也可能不相等,根據(jù)相關(guān)實現(xiàn)的設(shè)計者認(rèn)為哪一個更有效率來決定。在大部分系統(tǒng)中,兩個相等的定長數(shù)會是完全一樣的,但是不同類型的數(shù)字并不相等(特別是短精度)。Common Lisp提供了一般用的四個相等斷言。所有四個斷言都是字母eq開頭的,后面的字符更多意味著這個斷言會考慮更多的對象。最簡單的斷言就是eq,用來測試嚴(yán)格相等的對象。而后是eql用來測試eq相等或者相等的數(shù)字。Equal測試那些eql相等的對象或者每一個元素eql相等的列表或者字符串。最后,equalp和equal類似,只是他會匹配大小寫的字母與和不同類型的數(shù)字。下面的表格總結(jié)了四個相等斷言的應(yīng)用結(jié)果。問好的意思是結(jié)果根據(jù)實現(xiàn)不同而不同:兩個eql相等的整型數(shù)可能eq并不相等。


相等斷言比較.JPG

另外,還有一些特殊用途的想等斷言,比如=,tree-equal,char-equal,和string-equal,是分別用來比較數(shù)字,樹,字符和字符串的。
3.5 操作序列的函數(shù)

Common Lisp是一個處在過去的Lisp和未來的Lisp的過渡版本。這點上在序列操作的函數(shù)上最顯而易見。最早期的Lisp值會處理符號,數(shù)字,列表,并且提供了像append和length這樣的類表函數(shù)。更現(xiàn)代的Lisp版本加上了對于向量,字符串和其他數(shù)據(jù)結(jié)構(gòu)的支持,并且引入了術(shù)語,序列來統(tǒng)一指稱向量和列表。(一個向量就是一個一位數(shù)組。他比列表表現(xiàn)的更簡潔,因為像狼不需要存儲rest指針。獲取第n個元素的操作上,向量比列表也更加高效,因為向量不需要遍歷指針鏈)現(xiàn)代Lisp也支持字符向量組成的字符串,因此也是序列的一個子類型。
隨著新的數(shù)據(jù)類型到來,對于操作這個類型的函數(shù)的命名問題也出現(xiàn)了。在一些情況下,Common Lisp選擇沿用老的函數(shù),length可以同時應(yīng)用到列表和向量上。在其他情況下,老的名字會專門為列表函數(shù)保留,新的名字會為更加通用的序列函數(shù)發(fā)明出來。例如,append和mapcar只工作在列表上,,但是concatenate和map可以操作任意類型的序列。還有一些情況,會為新的數(shù)據(jù)結(jié)構(gòu)發(fā)明新的函數(shù)。例如,有7個函數(shù)來取出一個序列中的第n個元素。最通用的就是elt,可以操作任意序列類型,但是也有特別的函數(shù)來操作列表,數(shù)組,字符串,比特向量,簡單比特向量和簡單向量。令人疑惑的是,nth函數(shù)是唯一一個將索引參數(shù)放在第一個參數(shù)的函數(shù):
(nth n list)
(elt sequence n)
(aref array n)
(char string n)
(bit bit vector n)
(sbit simple-bit vector n)
(svref simple-vector n)
根據(jù)需要,最重要的序列函數(shù)列在本章的其他地方。

3.6 維護(hù)表格的函數(shù)

Lisp中列表可以用來表示一個一維的對象序列。由于列表豐富的特性,經(jīng)常用作其他的用途,比如表示一張信息的表格。聯(lián)合列表是用來實現(xiàn)表格的一種列表類型。一個聯(lián)合列表就是一個點對的列表,每一對都由一個key和一個值組成。和在一起,這個點對的列表就形成了一張表格:給出一個key,我們就個可以根據(jù)這個key找出對應(yīng)的值,或者核實這個表格中沒有這樣的key。下面是一個例子,用的是各個州的名字和他們的兩字母縮寫。使用的函數(shù)是assoc,返回的是key和值的對(如果有的話)。為了獲得值,之后還要用cdr函數(shù)來處理assoc的結(jié)果。
(setf state-table
‘((AL . Alabama) (AK . Alaska) (AZ . Arizona) (AR . Arkansas)))
>(assoc ‘AK state-table) => (AK . ALASKA)
>(cdr (assoc ‘AK state-table)) => ALASKA
>(assoc ‘TX state-table) => NIL
如果想要根據(jù)值而不是key來檢索列表,就要使用函數(shù)rassoc
>(rassoc ‘Arizona table) => (AZ . ARIZONA)
> (car (rassoc 'Arizona table) => AZ
使用assoc來管理一張表是很簡單的,但是也有一個缺點;我們檢索一個元素的時候,每一次都不得不搜索整張表格。如果列表很長的話,那么效率就不高。
另一種管理表格的方式是使用哈希表。這是一種專門高效管理大量數(shù)據(jù)的設(shè)計,但是對于規(guī)模較小的表格來說,它的開銷不是能接受的。函數(shù)gethash和很多get開頭的函數(shù)一樣接受兩個參數(shù),一個key和一個值。表格本身使用一個對make-hash-table的調(diào)用來初始化,然后用一個使用gethash的setf來修改。
(setf table (make-hash-table)
(setf (gethash 'AL table) 'Alabama)
(setf (gethash 'AK table) 'Alaska)
(setf (gethash 'AZ table) 'Arizona)
(setf (gethash 'AR table) 'Arkansas)
接下來我們就可以這么獲取值
> (gethash 'AK table) => ALASKA
> (gethash 'TX table) => NIL
函數(shù)remhash會從一個哈希表中刪除一個鍵值對,clrhash會移除所有的鍵值對,還有maphash可以用來定位所有的鍵值對。哈希表的key是不收限制的,可以指向任何Lisp對象。還有更多的細(xì)節(jié)在Common Lisp的哈希表實現(xiàn)中,還有一個擴(kuò)展的理論框架。
第三種表現(xiàn)表格的方式是用屬性列表。一個屬性列表就是一個可變的鍵值對列表。屬性列表(有些時候叫做p-lists或者plists)和聯(lián)合列表(a-lists或者alists)是很相似的:

a-list: (key1 . val1) (key2 . val2) … (keyn . valn))
p-list: (key1 val1 key2 val2 … keyn valn)
根據(jù)給定的條件,需要使用哪一種結(jié)構(gòu)在兩種結(jié)構(gòu)間要做出選擇。他們都是同一種信息的少許不同的表現(xiàn)形式。差別在于他們是如何被使用。每一個符號都有一個相關(guān)聯(lián)的屬性列表,這意味著我們可以將一個屬性或者值對,直接關(guān)聯(lián)在符號上。大部分程序只是用很少的不同屬性但是對于每一個屬性卻有很多屬性值對。因此,每一個符號的p-list會是比較短的。在我們例子中,我們只對一個屬性感興趣:每一個所寫的關(guān)聯(lián)州名。意思是,屬性列表實際上非常簡短,一個縮寫只對應(yīng)一個屬性,而不是在關(guān)聯(lián)列表中對應(yīng)50個對的列表。
屬性值可以用函數(shù)get獲取,他接受兩個參數(shù),第一個是一個我們正在檢索的信息的符號,第二個就是我們想要的符號的屬性。Get返回屬性的值,如果有的話。屬性值對可以用一個setf的形式的符號來存儲。一張表格可以構(gòu)建如此下:
(setf (get 'AL 'state) 'Alabama)
(setf (get 'AK 'state) 'Alaska)
(setf (get 'AZ 'state) 'Arizona)
(setf (get 'AR 'state) 'Arkansas)
現(xiàn)在就可以用get來獲取值:
> (get 'AK 'state) => ALASKA
> (get 'TX 'state) => NIL
這樣子效率就提高了,因為我們可以直接從一個度好的單個屬性中獲取值,而不必在意擁有屬性的富豪數(shù)量。然而,如果給定的符號有超過一個屬性,之后我們還是不得不線性檢索屬性列表。請注意在屬性列表中,沒有雨rassoc對應(yīng)功能的函數(shù);如果你想獲得一個州名的縮寫,你可以存儲一個縮寫對應(yīng)的州名屬性,但是那會是一個獨立的setf形式:
(setf (get 'Arizona 'abbrev) 'AZ)
事實上,當(dāng)遠(yuǎn),屬性和值都是符號的時候,使用屬性的可能就很小了。我們可以模仿a-list的方法,用一個符號列出所有的屬性,在函數(shù)中使用setf來設(shè)置symbol-plist(會給出一個符號的完整屬性列表):
(setf (symbol-plist ‘state-table)
'(AL Alabama AK Alaska AZ Arizona AR Arkansas))
> (get 'state-table 'AL) => ALASKA
> (get 'state-table 'Alaska) => NIL
屬性列表在Lisp中有很長的歷史,但是哈希表被引入之后就開始失寵了。避免使用屬性列表有兩個主要的理由,第一,因為符號和他的屬性列表是全局的,當(dāng)要合并兩個程序的時候很容易產(chǎn)生沖突,由于不同的目的的話,就不能一起工作。甚至一個符號在兩個程序中使用不同屬性的話,也會互相拖后腿。第二,屬性列表是一團(tuán)亂麻,如果表格的實現(xiàn)是屬性列表,沒有什么函數(shù)可以快速的移除元素。相對的,哈希表中有clrhash,或者將一個聯(lián)合列表設(shè)置成nil。

3.7 操作樹的函數(shù)

很多Common Lisp函數(shù)將表達(dá)式((a b) ((c)) (d e))看做一個三元素的序列,但是也有一些函數(shù)會把他看做一個有5個非空葉子節(jié)點的樹。函數(shù)copy-tree會創(chuàng)建一個樹的拷貝,函數(shù)tree-equal會測試兩個樹在組合單元層面是不是相等。這這方面tree-equal和equal相似,但是tree-equal更加強大,因為他允許:test關(guān)鍵字:
>(setf tree ‘((a b) ((c)) (d e)))
>(tree-equal tree (copy-tree tree)) => T
(defun same-shape-tree (a b)
“Are two trees the same expect for the leaves?”
(tree-equal a b :test #’true))
(defun true (&rest ignore) t)
>(same-shape-tree tree ‘((1 2) ((3)) ( 4 5))) => T
>(same-shape-tree tree ‘((1 2) (3) (4 5))) => NIL
下面的圖片是顯示((a b) ((c)) (d e))在組合單元層面的表現(xiàn)

一棵樹的組合單元形式.JPG

還有兩個函數(shù)是將原來舊的表達(dá)式用一個新的表達(dá)式替換。Subst會替換一個簡單的值,而sublis會用聯(lián)合列表的形式(old . new)對,來替換列表。請注意在alist中舊值和新值的順序,sublis和subst的參數(shù)順序是反的。名字sublis是一個字符縮寫,而且有些讓人困惑,實際上更好的名字是subst-list。
>(subst ‘new ‘old ‘(old ((very old)))) =>(New ((VERY NEW)))
>(sublis ‘((old . new)) ‘(old ((very old)))) => (NEW ((VERY NEW)))
>(subst ‘new ‘old ‘old) => ‘NEW

(defun English->French (words)
?(sublis ‘((are . va) (book . libre) (friend . ami)
? ? (hello . bonjour) (how . cmment) (my . mon)
? ? (red . rouge) (you . tu))
?words))
>(English->French ‘(hello my friend – how are you taoday?)) => (BONJOUR MON AMI – COMMENT VA TU TODAY?)

3.8 操作數(shù)字的函數(shù)

最常用的操作數(shù)字的函數(shù)都列印在這里,不常用的函數(shù)就省略了。

表達(dá)式 計算結(jié)果 功能含義
(+ 4 2) =>6 加法
(- 4 2) =>2 減法
(* 4 2) =>8 乘法
(/ 4 2) =>2 除法
(> 100 99) =>t 大于(>=大于等于)
(= 100 100) =>t 等于(/=不等于)
(< 99 100) =>t 小于(<=小于等于)
(random 100) =>42 0到99 的隨機(jī)數(shù)
(expt 4 2) =>16 求冪(還有exp和log函數(shù))
(sin pi) =>0.0 sin函數(shù)(還有cos,tan等等)
(asin 0) =>0.0 sin的反函數(shù)arcsin(還有acos,atan等等)
(min 2 3 4) =>2 最小數(shù)(還有max)
(abs -3) =>3 絕對值
(sqrt 4) =>4 平方根
(round 4.1) =>4 化為整數(shù)(還有truncate,floor,ceiling)
(rem 11 5) =>1 余數(shù)(還有mod)
3.9 操作集合的函數(shù)

列表的重要用處之一就是表示集合。Common Lisp提供了函數(shù)來這樣操作列表。例如,一般表現(xiàn)集合的r={a,b,c,d}和s={c,d,e},我們可以這么做:
> (setf r '(a b cd)) => (A BCD)
> (setf s '(c de)) => (C D E)
> (intersection r s) => (C D)
有些實現(xiàn)會返回(C D)作為答案,另外一些實現(xiàn)會返回(D C)。相同的集合,都是合法的,你的程序也不應(yīng)該依賴結(jié)果的元素順序。下面是一些操作集合的主要函數(shù)

表達(dá)式 計算結(jié)果 功能含義
(intersection r s) =>(c d) 兩個集合的共有元素
(union r s) =>(a b c d e) 兩個集合所有的元素
(set-difference r s) =>(a b) 在r中但是不在s中的元素
(member ‘d r) =>(d) 檢查一個元素是不是集合的成員
(subset s r) =>nil 子集關(guān)系判斷
(adjoin ‘b s) =>(b c d e) 給集合加一個元素
(adjoin ‘c s) =>(c d e) 加一個元素,但是不加重復(fù)元素

給定一個特殊的領(lǐng)域,將集合用比特位序列來體現(xiàn)也是可以的。例如,如果每一個集合(a b c d e)的子集合就是討論的范圍,那么我們就可以使用位序列11110來表示(a b c d),00000來表示空集合,11001來表示(a b e)。比特位序列在Common Lisp中可以使用比特向量來表示,或者是一個整數(shù)的為禁止形式,例如,(a b e)可以用比特向量井號星號11011表示,或者用整數(shù)25表示,也可以寫成井號b11001。
使用比特向量的好處就是節(jié)省集合編碼的空間,當(dāng)然是在一個預(yù)定的語境下面。計算會更加迅速,因為計算機(jī)的底層指令集可以一次性處理32位。
Common Lisp提供了一個完整的函數(shù)補充,來操作比特向量和整型數(shù)。下面的表格列出了一些,以及他們對應(yīng)的列表函數(shù)。

處理列表的函數(shù) 處理整型數(shù)的函數(shù) 處理比特向量的函數(shù)
Intersection logand bit-and
Union logior bit-ior
Set-difference logandc2 bit-andc2
Member logbitp bit
Length logcount

例子
(intersection '(a b c d) '(a be)) => (A B)
(bit-and #*11110 #*11001) => #*11000
(logand #b11110 #b11001) => 24 == #b11000

3.10 具有破壞性的函數(shù)

在數(shù)學(xué)中,一個函數(shù)知識對給定的輸入?yún)?shù)計算輸出的值。函數(shù)不會真的做任何事情,僅僅是計算結(jié)果。例如,如果說x = 4,y = 5并且將加函數(shù)應(yīng)用在這兩個參數(shù)x和y上,期待的答案一定是9.如果說,計算之后的x的值是什么?x的值被改變的話肯定會讓人大吃一斤。在數(shù)學(xué)中,將運算符應(yīng)用到x不會對x本身的值有任何作用。
在Lisp中,有一些函數(shù)步進(jìn)可以計算結(jié)果,也能產(chǎn)生一些效果。這些函數(shù)就不是數(shù)學(xué)概念上的函數(shù)了,在其他語言中被稱作過程。當(dāng)然,Lisp中的大部分函數(shù)是數(shù)學(xué)函數(shù),但是也有不是的。他們這種破壞性函數(shù)在特定的情況下是很有用的。
> (setf x '(a b c)) => (A B C)
> (setf y '(1 2 3)) => (1 2 3)
> (append x y) => (A B C 1 2 3)
Append是一個純粹的函數(shù),所以在對append的調(diào)用求值之后,我們可以肯定x和y還是原來的值。
> (nconc x y) => (A B C 1 2 3)
> X => (A B C 1 2 3)
> y ?=> (1 2 3)
函數(shù)nconc和appned的計算結(jié)果是相同的,但是卻有一個副作用,他會更改第一個參數(shù)的值,所以他被稱作是hi一耳光破壞性函數(shù),因為他會破壞原有的結(jié)構(gòu),代之以一個新的結(jié)構(gòu)。這意味著對使用這個nconc函數(shù)的程序猿會有一個概念上的負(fù)擔(dān)。他必須知道第一個參數(shù)的值將會有所變化,這種考慮要比使用非破壞性函數(shù)復(fù)雜多了,因為程序員需要關(guān)心函數(shù)調(diào)用的結(jié)果和副作用。
Nconc的好處就是他不會使用任何存儲空間,append必須做一個X的拷貝之后追加到y(tǒng)上,nconc不需要拷貝任何東西。而是只要更改x的最后一個元素的rest部分,指向y就可以了。所以當(dāng)需要保留存儲空間的時候就需要使用破壞性函數(shù),但是也要意識到他的后果。
除了nconc還有很多n開頭的破壞性函數(shù),包括nreverse,nintersection,nunion,nset-difference和nsubst。很重要的一個例外就是delete,他是非破壞性函數(shù)remove的對應(yīng)版本。當(dāng)然,特殊形式setf也用來更改結(jié)構(gòu),但是他是最危險的非破壞性函數(shù),因為很容易忽視他們的效果。
【h】更改結(jié)構(gòu)練習(xí)。寫一個程序來扮演游戲二十個問題的回答者角色。程序的用戶會在腦中想像熱和種類的事物。程序會問人問題,用戶必須回答,是,否,猜中的時候就回答猜對了。如果程序猜測,超過了二十個問題,就會放棄猜測直接問是什么東西。一開始程序玩的很藍(lán),但是每一次運行,他會記憶用戶的回答,并在之后的猜測中使用。

3.11 數(shù)據(jù)類型概覽

本章是圍繞函數(shù)來組織的,當(dāng)類似的函數(shù)就放在一起。但是在Common Lisp的概念里還有另一種組織方式,就是通過不同的數(shù)據(jù)結(jié)構(gòu)來組織。這樣子的理由主要有兩點:第一,他會給出一個可選的功能種類的不同方式。第二,數(shù)據(jù)類型本身就是Common Lisp語言的對象,有很多函數(shù)用來操作數(shù)據(jù)類型。還有就是主要在做出聲明和測試對象(使用typecase宏)。
下面的表格是最常用的數(shù)據(jù)類型:

數(shù)據(jù)類型 樣例 含義解釋
Character 字符 #\c 單個的字符,數(shù)字或者標(biāo)點符號標(biāo)記
Number 數(shù)字 42 最常用的數(shù)字就是浮點數(shù)和整型數(shù)
Float 浮點數(shù) 3.14159 使用十進(jìn)制小數(shù)點的數(shù)字
Integer 整型數(shù) 42 一個整數(shù),定長的或者變長的數(shù)字
Fixnum 定長數(shù) 123 用單字長存儲的整型數(shù)
Bignum 變長數(shù) 123456789 不綁定長度的整型數(shù)
Function 函數(shù) #’sin 帶一個參數(shù)列表的函數(shù)
Symbol 符號 sin 符號可以用來命名函數(shù)或者變量,或者可以指向自己的對象
Null 空 nil 對象nil是唯一的空類型對象
Keyword 關(guān)鍵字 :key 關(guān)鍵字是符號類型的子類型
Sequence 序列 (a b c) 序列包括列表和向量
List 列表 (a b c) 一個列表就是一個組合單元cons或者是空null
Vector 向量 #(a b c) 向量是序列的子類型
Cons 組合 (a b c) 組合就是非空列表
Atom 原子 t 原子就是,不是組合的話就是一個原子
String 字符串 ”abc” 字符串就是字符向量的一個類型
Array 數(shù)組 #1A(a b c) 數(shù)組包含了向量和高維數(shù)組
Structure 結(jié)構(gòu) #S(type …) 結(jié)構(gòu)通過defstruct來定義
Hash-table 哈希表 哈希表通過make-hash-table來創(chuàng)建

幾乎每一種類型都有一個分辨器斷言——就是一種判斷是不是這個類型的函數(shù)。一般來說,一個斷言的返回值只有兩種:真或者假。在Lisp中,假的值就是nil,其他的都被認(rèn)為是真值,一般來說真值的表示是t。一般來說,分辨器斷言的名字是用類型名加上字母p來組成的:characterp就是用來分辨字符,numberp就是用來分辨數(shù)字,等等等等。例如,(numberp 3)的返回值是t,因為3是一個數(shù)字,但是(number “x”)就會返回nil,因為x不是一個數(shù)字是一個字符串。
Common Lisp有一個不好,就是沒有給所有類型實現(xiàn)分辨器,定長數(shù),變長數(shù),序列和結(jié)構(gòu)就沒有分辨器斷言。有兩個分辨器null和atom是不以p結(jié)尾的斷言。還有就是在p和之前的字符有一個連字符的,比如hash-table-p,是因為之前的名字就有連字符。另外,所有由defstruct生成的分辨器斷言p之前都有一個連字符。
函數(shù)type-of返回的是它的參數(shù)的子類型,typep是用來測試一個對象是不是指定的類型,函數(shù)subtype測試的是一個類型是不是可以分成另一個子類型。例如:
> (type-of 123) => FIXNUM
> (typep 123 'fixnum) => T
> (typep 123 'number) => T
> (typep 123 'integer) => T
> (typep 123.0 'integer) => NIL
> (subtypep 'fixnum 'number) => T
在Common Lisp中的類型層次是有一些復(fù)雜。如上面的表格顯示,數(shù)字就有很多不同的類型,像123就可以被看做是定長,整型或者和數(shù)字類型。之后我們還會看到類型rational分?jǐn)?shù)和t。
類型層次是一個圖狀拓?fù)潢P(guān)系,而不僅僅是一個樹。例如,向量同時是一個序列和一個數(shù)組,雖然數(shù)組和序列互相之間不是屬于子類型關(guān)系。相似的null就同時是symbol和list的子類型。
下面的表格是一些不常用到的數(shù)據(jù)類型:

數(shù)據(jù)類型 樣例 含義解釋
T 42 每一個對象都是t類型
Nil 沒有對象是nil類型的
Complex 復(fù)數(shù) #C(0 1) 虛數(shù)類型
Bit 位 0 比特位,0或者1
Rational 有理數(shù) 3/2 有理數(shù)包括整數(shù)和分?jǐn)?shù)
Ratio 分?jǐn)?shù) 2/3 精確地分?jǐn)?shù)
Simple-array 簡單數(shù)組 #1A(x y) 不可以替換或者改變的數(shù)組
Readtable 字符和可讀取含義的映射
Package 模塊形式的符號集合
Pathname #P”/usr/spool/mail” 文件或者目錄名
Stream 流 一個打開的文件的指針;用來讀取或者打印
Random-state 用來做random的種子的狀態(tài)

另外還有一些更加特殊的類型,比如short-float,compiled-function,和bit-vector。也可以構(gòu)造一些更加精確地類型比如,(vector (integer 0 3) 100),意思是一個100個元素的向量,每一個元素都是從0到3的整數(shù)。第10.1章節(jié)會有關(guān)于特定類型和使用的詳細(xì)介紹。
斷言可以用來判斷每一個數(shù)據(jù)類型,也可以用來根據(jù)一些特定的條件進(jìn)行判斷。例如oddp就用來判斷奇數(shù),string-greaterp用來判斷一個字符串是不是在字符意義上比另一個更大。

3.12 輸入輸出I/O

Lisp中的輸入是非常簡單的,因為用戶可以用一個完整的語法語義解析器。這個解析器叫做read。他用來讀取,返回一個Lisp表達(dá)式。你也可以設(shè)計一個自己的read版本應(yīng)用來解析輸入。還有,read的輸入不一定是要合法的可以求值的Lisp表達(dá)式才可以。就是說,你可以讀取(“hello” cons zzz),就像讀取(+ 2 2)一樣。有些情況Lisp表達(dá)式也不能很好地運作,比如函數(shù)read-char就用來讀取單個字符,read-line就會把接下來的一行所有的內(nèi)容讀取,然后作為字符串返回。
從終端讀取輸入,函數(shù)read,read-char或read-line(沒有參數(shù)的話)就會分別反悔一個表達(dá)式,一個字符和一個字符串。從文件中讀取也是可以的,函數(shù)open和with-open-stream可以用來打開一個一個文件并關(guān)聯(lián)到一個流上,stream就是Lisp中對于文件輸入輸出描述符的名字。這三個讀取函數(shù)都接受三個可選的參數(shù)。第一個就是要讀取的流。第二個,如果為t,會在遇到文件末尾的時候提出錯誤。第二個參數(shù)如果為nil,第三個參數(shù)就是設(shè)置,遇到文件末尾的時候返回的值。
Lisp中的輸出類似于其他語言中的輸出,例如C語言。有一些底層的函數(shù)來做一個特定種類的輸出,也有一些通用的函數(shù)來做格式化輸出。函數(shù)print會在一個新行上打印任何對象,后跟一個空格。Prinl不開新行,也沒有后跟空格打印對象。兩個函數(shù)的打印形式都是用的read可以處理的方式。Lieu字符串”hello there”就會打印成”hello there”。函數(shù)princ被用作打印人類可以閱讀的形式。同樣的字符串就會打印成hello there,就把雙引號去掉了。那也就是說read就不能再回復(fù)原有的格式了;raed會將其解釋為兩個符號,而不是一個字符串。函數(shù)write接受是一個不同的關(guān)鍵字參數(shù),根據(jù)不同的設(shè)置可以將行為表現(xiàn)的像prinl和princ函數(shù)一樣。
輸出函數(shù)也接受一個流作為可選的參數(shù)。接下來,我們創(chuàng)建文件test.text,并且在里面打印兩個表達(dá)式。之后我們會打開文件讀取,嘗試讀回第一個表達(dá)式,一個字符和之后的兩個表達(dá)式。請注意read-char函數(shù)返回的是字符井號\G,后面read讀取字符OODBYE之后將他們轉(zhuǎn)化成一個符號。最后raed會遇到文件結(jié)束,返回的是一個特殊值,eof。
> (with-open-file (stream "test.text" :direction :output)
(print '(hello there) stream)
(prine 'goodbye stream) ? =>
GOODBYE ; and creates the file test.text
> (with-open-file (stream "test.text" :direction :input)
(list (read stream) (read-char stream) (read stream)
(read stream nil 'eof))) =>
((HELLO THERE) #\G OODBYE EOF)
函數(shù)terpri是中斷打印行(terminate print line)的縮寫,之后就跳過下一行。函數(shù)fresh-line也會挑貨下一行,除非輸出已經(jīng)在行首的話。
Common Lisp也提供了通用的輸出函數(shù),叫做format。Format的第一個參數(shù)總是一個流,往這個流中打印,使用t的話就是打印到終端上。第二個參數(shù)就是格式化字符串,format會珠子原樣輸出,除非碰到了格式指令,字符~打頭的一些指令。這些指令會告訴函數(shù)如何打印后面的參數(shù)。C中的printf函數(shù)和FORTRAN中的format函數(shù)和這里的格式化輸出概念很相似。
> (format t "hello, world")
hello,world
NIL
當(dāng)我們使用格式化控制指令和附加參數(shù)結(jié)合的時候,就會蘭道有意思的結(jié)果:
> (format t "~&~a plus ~s is ~f" "two" "two" 4)
two plus "two" is 4.0
NIL
&的意思是新開一行,a的意思是如princ函數(shù)打印的形式打印,s的意思是下一個參數(shù)用prinl的形式打印,f的意思就是浮點數(shù)格式打印數(shù)字。如果參數(shù)不是一個數(shù)字,那么就會用princ打印。Format的返回值總是nil。總共是有26個不同的格式化控制指令。下面是一些復(fù)雜的例子:
> (let ((numbers '(1 234 5))
?(format t "~&~{~r~^ plus ~} is ~@r"
? ?numbers (apply #'+ numbers)))
one plus two plus three plus four plus five is XV
NIL
指令r會將下一個參數(shù),應(yīng)該是數(shù)字,用英語的形式打印出來,而指令@r會將數(shù)字用羅馬數(shù)字打印出來。組合之靈{…}的意思是接受一個列表,根據(jù)大括號內(nèi)的格式化指令輸出每一個元素。最后指令~^會跳出大括號的循環(huán)中。如果說沒有更多元素的話。你可以看到format就像loop一樣,包含了幾乎一整個編程語言,也像loop一樣,不是很符合Lisp的語言風(fēng)格。

3.13 調(diào)試工具

在很多語言中,調(diào)試的策略有兩種,一種是編輯程序,擦呼吁一些打印語句,重新編譯然后再次嘗試。第二中就是使用調(diào)試工具,查看或者更改運行中的程序狀態(tài)。
Common Lisp對兩種策略都是認(rèn)可的,但是也提供了第三種:給程序加上注釋,注釋本身并不是程序的一部分,但是會有自動更改運行中程序的效果。第三種策略的好處就是一旦完成了之后就不需要退回去更改在第一種策略中留下的更改。另外Common Lisp提供了顯示程序信息的函數(shù)。不是一定只能依賴看源代碼了。
之前我們已經(jīng)見過,trace和untrace用來追蹤程序的調(diào)試信息。另一個有用的工具是step,可以再每一個子形式求值之前中斷執(zhí)行。形式(step 表達(dá)式)會在求值的同時但會表達(dá)式,三十會在給定的點暫停下來,允許用戶在下一步執(zhí)行之前查看計算,更改一些東西。這個命令是根據(jù)Lisp實現(xiàn)的不同來決定有沒有提供的,可以用問號來查看命令列表看有沒有。下面的一個例子是我們步進(jìn)查看一個表達(dá)式兩次,一次是在每一次子求值之前暫停,另一次是跳轉(zhuǎn)到每一次函數(shù)調(diào)用。在這個實現(xiàn)中,命令就是控制字符,所有他們不會在輸出中出現(xiàn)。所有的輸出,包括左右箭頭,都是有步進(jìn)器打印的,我沒有加任何標(biāo)記:
> (step (+ 3 4 (* 5 6 (/ 7 8))))
<= (+ 3 4 (* 5 6 (/ 7 8)))
?<=3=>3
?<=4=>4
?<= (* 5 6 (/ 7 8))
? ?<=5=>5
? ?<=6=>6
? ?<= (/ 7 8)
? ? ?<=7=>7
? ? ?<=8=>8
? ?<= (/ 7 8) => 7/8
?<= (* 5 6 (I 7 8)) => 105/4
<= (+ 3 4 (* 5 6 (I 7 8))) => 133/4
133/14
> (step (+ 3 4 (* 5 6 (/ 7 8))))
<= (+ 3 4 (* 5 6 (/ 7 8)))
?/: 7 8 => 7/8
?*: 5 6 7/8 => 105/4
?+: 3 4 105/4 => 133/4
<= (+ 3 4 (* 5 6 (I 7 8))) => 133/4
133/14
函數(shù)describe,inspect,documentation,和apropos都提供了當(dāng)前程序的狀態(tài)信息。Propos打印的關(guān)于所有匹配參數(shù)名的符號信息。
> (apropos 'string)
MAKE-STRING function (LENGTH &KEY INITIAL-ELEMENT)
PRIN1-TO-STRING function (OBJECT)
PRINC-TO-STRING function (OBJECT)
STRING function (X)

知道對象名字之后,describe函數(shù)就可以用來獲取進(jìn)一步的信息
> (describe 'make-string)
Symbol MAKE-STRING is in LISP package.
The function definition is #<FUNCTION MAKE-STRING -42524322>:
NAME: MAKE-STRING
ARGLIST: (LENGTH &KEY INITIAL-ELEMENT)
DOCUMENTATION: "Creates and returns a string of LENGTH elements,
all set to INITIAL-ELEMENT."
DEFINITION: (LAMBDA (LENGTH &KEY INITIAL-ELEMENT)
(MAKE-ARRAY LENGTH :ELEMENT-TYPE 'CHARACTER
:INITIAL-ELEMENT (OR INITIAL-ELEMENT
#\SPACE)))
MAKE-STRING has property INLINE: INLINE
MAKE-STRING has property :SOURCE-FILE: #P"SYS:KERNEL; STRINGS"
> (describe 1234.56)
1234.56 is a single-precision floating-point number.
Sign 0, exponent #0211, 23-bit fraction #06450754
如果只是想要符號的文檔字符串,函數(shù)documentation會取得:
> (documentation 'first 'function) => "Return the first element of LIST."
> (documentation 'pi 'variable) => "pi"
如果你想查看或者更改復(fù)雜結(jié)構(gòu)的租金啊,工具inspect可以使用。在一些實現(xiàn)中,他會調(diào)用一個基于窗口的瀏覽器。
Common Lisp也提供一個在出錯的時候自動進(jìn)入的調(diào)試器,錯誤或許是無意的或許是語義的深層錯誤,都會自動進(jìn)入調(diào)試器。調(diào)試器的細(xì)節(jié)根據(jù)實現(xiàn)不同而不同,但是進(jìn)入調(diào)試器的方法是有標(biāo)準(zhǔn)的。函數(shù)break會進(jìn)入調(diào)試器然后打印條可選的信息。這是有意設(shè)置成為調(diào)試斷點的主要方法。Break只為調(diào)試目的服務(wù);當(dāng)一個程序被認(rèn)為是可以運行,所有對break的調(diào)用都應(yīng)該移除。然而,使用函數(shù)error,cerror,assert,或者chek-type做一個非常條件下的檢查或許是個好主意,這些函數(shù)我們在之后會介紹。

3.14 防錯工具

在代碼中包含防錯檢查是除了正常調(diào)試外的一個很好地做法。防錯代碼會檢測錯誤并且可能會做出正確的操作。
函數(shù)error和cerror就是用來在出錯的時候發(fā)出信號。這些函數(shù)調(diào)用即使是在調(diào)試之后也會保留。函數(shù)error接受一耳光格式化字符串和一些可選參數(shù)。如果發(fā)出的是致命錯誤信號,程序就會停止運行,并且不讓用戶再啟動了。例如:
(defun average (nubers)
?(if (null numbers)
? ?(error “Average of the ampty list is undefined.”)
? ?(/ (reduce #’+ numbers)
? ? ?(length numbers))))
在很多情況下,致命錯誤是很猛的。還有一個函數(shù)cerror是可繼續(xù)錯誤的縮寫。Cerror接受兩個格式化字符串;第一個會打印信息,顯示如果我們繼續(xù)會發(fā)生什么,第二個打印錯誤信息本身。Cerror實際上不會做任何修正錯誤的操作,只不過允許用戶發(fā)出認(rèn)可錯誤繼續(xù)運行的信號罷了。在下面的實現(xiàn)中,用戶通過鍵入:continue來繼續(xù)運行。在ANSI Common Lisp中,還有其他的方式來制定繼續(xù)的選項。
(defun average (numbers)
?(if (null numbers)
? ?(progn
? ? ?(cerror "Use 0 as the average."
? ? ? ?"Average of the empty list is undefined.")
? ? ?? 0)
? ? ?(/ (reduce #'+ numbers)
? ? ? ?(length numbers))))
> (average ' () )
Error: Average of the empty list is undefined.
Error signaled by function AVERAGE.
If continued: Use 0 as the average.
>> :continue
0
在這個例子中如果加入錯誤檢查的話,會讓代碼的長度倍增。一般不會這么做,這也是工作在給定輸入的代碼和工作在所有錯誤環(huán)境下的代碼的一個重大區(qū)別。Common Lisp嘗試使用一個實現(xiàn)一些特殊形式來提供錯誤檢查。Ecase形式就是error case的縮寫,他就像一個正常的case形式,除了一點,要是沒有情況滿足條件,就會報錯。還有ccase是continuable case的縮寫。和ecase一樣,除了錯誤是可繼續(xù)的。系統(tǒng)要求測試對象有一個新的值,知道說用戶支持了匹配的對象之一。
為了讓錯誤檢查不會帶來代碼規(guī)模的膨脹,Common Lisp提供了特殊形式check-type和assert。如其名,check-type用來檢查參數(shù)類型。如果參數(shù)的類型錯誤,就會報出一個可繼續(xù)錯誤。例如:
(defun sqr (x)
?“Multiply x by itself.”
?(check-type x number)
?(* x x))
如果sqr的參數(shù)不是一個數(shù)字的話就對報出一個適當(dāng)?shù)腻e誤信息:
> (sqr "hello")
Error: the argument X was "hello", which is not a NUMBER.
If continued: replace X with new value
>> :continue 4
16
Assert比check-type更加通用,在最簡單的形式中,assert測試一個表達(dá)式,根絕返回值的真假來報出信號:
(defun sqr (x)
?"Multiply x by itself."
?(assert (numberp x))
?(* x x))
出現(xiàn)這種斷言想再繼續(xù)是不可能的了。但是可以給assert一系列的可修改的參數(shù),嘗試使得assert的返回值為真。下面的例子中,變量x就是唯一一個可以改變的:
(defun sqr (x)
?"Multiply x by itself."
?(assert (numberp x) (x))
? (* x x))
如果違法了這個斷言,就會打印出一個錯誤信息,用戶會被給與一個可繼續(xù)的選項來更改x。如果x的值滿足了斷言,程序就會繼續(xù),assert的返回值永遠(yuǎn)是nil。
最后,對于想要更多的叢植錯誤信息的用戶可以提供一個格式控制字符串和可選選項。所以最復(fù)雜的assert語法是:
(assert測試形式部分 (位置…) 格式化控制字符串 格式化參數(shù)…)
這里是另一個例子。程序執(zhí)行前斷言檢測,熊喝的麥片粥是不是太燙了還是太冷了。
(defun eat-porridge (bear)
? (assert (< too-cold (temperature (bear-porridge bear)) too-hot)
? ? (bear (bear-porridge bear))
? ? “~a’s porridge is not just right:~a”
? ?Bear (hotness (bear-porridge bear)))
?(eat (bear-porridge bear)))
在下面的交互過程中,斷言失敗了,打印了程序的錯誤信息,還有兩個可以繼續(xù)的可能選項。用戶選擇一個,調(diào)用make-porridge輸入一個新的值,函數(shù)就成功繼續(xù)了。
> (eat-porridge momma-bear)
Error: #<MOMMA BEAR>'s porridge is not just right: 39
Restart actions (select using :continue):
0: Supply a new value for BEAR
1: Supply a new value for (BEAR-PORRIDGE BEAR)
>> :continue 1
Form to evaluate and use to replace (BEAR-PORRIDGE BEAR):
(make-porridge :temperature just-right)
Nil
如果程序運行OK的話,好像也不必要浪費時間來寫斷言。但是對于很多不是全知全能的程序員,bug總是層出不窮,花在排錯上的時間還不如寫斷言來的省時省力。
無論何時,擬開發(fā)了一個復(fù)雜的數(shù)據(jù)結(jié)構(gòu),比如某種數(shù)據(jù)庫,開發(fā)一個對應(yīng)的一致性檢查器總是一個好的做法。一致性檢查就是看看整個數(shù)據(jù)結(jié)構(gòu)測試所有可能的錯誤。當(dāng)發(fā)現(xiàn)了一個新的錯誤,這個錯誤的檢查就應(yīng)該成為一致性檢查的一部分了。調(diào)用一致性檢查是最快的幫助定位數(shù)據(jù)結(jié)構(gòu)中bug的方式。
另外,保持進(jìn)行一些測試也是很好的作法,當(dāng)程序更改之后,很容易將之前排出的bug重新引回程序中。這種發(fā)福測試叫做回歸測試,Waters在1991年詐尸了一個維護(hù)回歸測試的工具集。但是也是很簡單的,可以使用一系列assert調(diào)用來委會一個非常規(guī)測試。
(defun test-ex ()
"Test the program EX on a series of examples."
(init-ex) ; Initialize the EX program first.
(assert (equal (ex 3 4) 5))
(assert (equal (ex 5 0) 0))
(assert (equal (ex 'x 0) 0)))

時間測試工具

一個完整的程序不僅僅可以給出正確的輸出,還要考慮程序的效率。(time 表達(dá)式)可以用來看看執(zhí)行這個表達(dá)式用了多少時間,一些實現(xiàn)也會打印靜態(tài)的內(nèi)存占用空間:
> (defun f (n) (dotimes (i n) nil)) => F
> (time (f 10000)) => NIL
Evaluation of (F 10000) took 4.347272 Seconds of elapsed time,
including 0.0 seconds of paging time for 0 faults, Consed 27 words.
> (compile 'f) => F
> (time (f 10000)) => NIL
Evaluation of (F 10000) took 0.011518 Seconds of elapsed time,
including 0.0 seconds of paging time for 0 faults, Consed 0 words.
信息顯示,編譯版本的程序快了300倍,而且用的空間更少。大部分嚴(yán)謹(jǐn)?shù)腃ommon Lisp程序員都會使用編譯版本的程序工作。然而一般看來,在開發(fā)程序的伊始就考慮效率問題是不大合適的。更好的是設(shè)計一個靈活性比較高的程序,先運行起來,之后在修改的更加高效。換句話說,就是講開發(fā)階段和優(yōu)化階段分開。第9章和第10章會給出提高效率的更多細(xì)節(jié),第25章會給出更多關(guān)于調(diào)試和排錯技術(shù)的建議。

3.15 求值

Lisp中有三個函數(shù)是做求值的:funcall,apply和eval。Funcall用于吧一個函數(shù)應(yīng)用在獨立的函數(shù)上,apply是用于將一個函數(shù)應(yīng)用到參數(shù)的列表上。實際上apply可以再最后的參數(shù)之前有一個或者多個獨立的具有完整形式的參數(shù),函數(shù)或者特殊形式,之后又參數(shù)或者原子。下面的五個語句是相等的:
> (+ 1 2 3 4) => 10
> (funcall #'+ 1 2 3 4) => 10
> (apply #'+ '(1 2 3 4)) => 10
> (apply #'+ 1 2 '(3 4)) => 10
> (eval '(+ 1 234)) => 10
過去的觀念是,eval就是Lisp靈活性的關(guān)鍵所在。在現(xiàn)代Lisp版本中,使用靜態(tài)域的版本,比如Common Lisp,eval的使用越來越少(事實上,Scheme中就沒有eval)。代之的是,程序員虎使用lambda表達(dá)式創(chuàng)建一個新函數(shù),之后用apply或者funcall來調(diào)用函數(shù)。一般來說,如果你發(fā)現(xiàn)你正在使用eval,那一般就是你做錯了。

3.16 閉包

創(chuàng)建一個新函數(shù)的意思到底是什么?當(dāng)然每一次一個特殊形式function或者井號單引號的簡略形式被求值的話,一個函數(shù)就會被反悔。但是在這個例子中我們看到,返回的函數(shù)總是相同的。
> (mapcar #'(lambda (x) (+ x x)) '(1 3 10)) => (2 6 20)
每一次我們對lambda表達(dá)式求值,返回的函數(shù)就會對后面的參數(shù)進(jìn)行加倍操作。然而在一般情況下,一個函數(shù)是由函數(shù)的主體和函數(shù)像伴隨的函數(shù)引用自由詞域變量組成的。這樣的一個組合叫做詞法閉包,或者簡稱閉包,因為語法變量是閉包在函數(shù)中的。看下面的例子:
(defun adder (c)
?"Return a function that adds c to its argument."
?#' (l ambda (x) (+ xc)))
> (mapcar (adder 3) '(1 3 10)) => (4 6 13)
> (mapcar (adder 10) '(1 3 10)) => (11 13 20)
每一次我們給c不同的值來調(diào)用adder,他都會創(chuàng)建一個不同的函數(shù),函數(shù)會把c加到它的參數(shù)上。既然每一次對adder的調(diào)用都創(chuàng)建了一個不同的本地變量c,那么每一次adder返回的函數(shù)也會是一個不一樣的函數(shù)。下面是另一個例子,函數(shù)bank-account返回一個可以用來作為銀行賬號形式的閉包。這個閉包取得本地變量balance。必報的主體提供代碼來訪問和修改本地變量。
(defun bak-account (balance)
?“Open a bank account starting with the given balance.”
?#’(lambda (action amount)
? ?(case action
? ? ?(deposit (setf balance (+ balance amount)))
? ? ?(withdraw (setf balance (- balance amount))))))
下面的涼席對于bank-account 的調(diào)用創(chuàng)建了兩個不同的閉包,每一個詞法變量balance都有不同的值。隨后對兩個閉包的調(diào)用分別改變了它們的變量的值,但是兩個賬戶之間是沒有混淆的。
> (setf my-account (bank-account 500.00)) => #<CLOSURE 52330407>
> (setf your-account (bank-account 250.00)) => #<CLOSURE 52331203>
> (funcall my-account 'withdraw 75.00) => 425.0
> (funcall your-account 'deposit 250.00) => 500.0
> (funcall your-account 'withdraw 100.00) => 400.0
> (funcall my-account 'withdraw 25.00) => 400.0
這種編程風(fēng)格會在第13章有更加詳細(xì)的介紹。

3.17 特殊變量

Common Lisp提供了兩種變量:詞法變量和特殊變量。對初學(xué)者來說,會把這個概念和其他語言總的全局變量進(jìn)行等同。但是這樣會導(dǎo)致一些問題。最好是將Common Lisp中的術(shù)語進(jìn)行獨立的理解。
Common Lisp默認(rèn)的變量都是詞法變量。詞法變量的引入是通過一些系那個let或者defun之類的語法結(jié)構(gòu)來引入的,而且他們的名字能被引用的范圍,在代碼中也是有限的。這個范圍被稱作變量的作用域。
變量作用域這個概念上,Common Lisp和其他語言是沒有區(qū)別的,可以叫做變量的范圍,或者生命周期。在其他語言中,范圍是等同于作用域的:在進(jìn)入一塊代碼的時候創(chuàng)建的本地變量,離開塊代碼的時候就會銷毀。但是因為Lisp中可以創(chuàng)建新的函數(shù),閉包,因此代碼中引用的變量的生存周期可以在離開代碼作用域之后任然繼續(xù)存在。再來看看bank-acount函數(shù),他創(chuàng)建了一個表現(xiàn)為銀行賬號的閉包:
(defun bank-account (balance)
?"Open a bank account starting with the given balance."
?#'(lambda (action amount)
? ?(case action
? ? ?(deposit (setf balance (+ balance amount)))
? ? ?(withdraw (setf balance (- balance amount))))))
函數(shù)引入了一個詞法變量balance。Balance的作用域就是函數(shù)的主體,因此對于balance的引用只能在作用域中進(jìn)行。那bank-account被調(diào)用和結(jié)束的時候發(fā)生了什么呢?作用域是結(jié)束了,但是balance的范圍還在延續(xù)。我們可以調(diào)用閉包,閉包可以引用變量balance,因為這個創(chuàng)建閉包的代碼是在balance的作用域中的。
總的來說,Common Lisp詞法變量是不一樣的因為他們可以獲取閉包,之后甚至在控制流之后指向他們的作用域。
現(xiàn)在我們來看看特殊變量,一個變量被稱作特殊,是要通過defvar或者defparamerter來定義。例如:
(defvar *counter* 0)
這樣就可以在程序的任何地方指向變量counter。這時類似于全局變量,不一樣的部分是,全局綁定可以通過本地綁定來進(jìn)行屏蔽。在大部分語言中,本地綁定會引入一個本地詞法變量,但是在Common Lisp中,特殊變量可以同時進(jìn)行本地的和全局的綁定。下面是例子:
(defun report ()
?(format t "Counter = -d " *counter*))
> (report)
Counter = 0
NIL
> (let (*counter* 100))
(report))
Counter = 100
NIL
> (report)
Counter = 0
NIL
這里對report有了三次調(diào)用。第一次和第三次,report都打印的是特殊變量counter的全局值。第二個調(diào)用,let形式引入了一個對特殊變量counter的新的綁定,之后在打印的值。一旦let的作用域結(jié)束,新的綁定就被銷毀,所以report就重新開始使用全局值。
總的來說,Common Lisp特殊變量的不同是因為他們有全局作用域,但是也承認(rèn)本地的(動態(tài))屏蔽的可行。請記住:一個詞法變量有詞法作用域和不確定的范圍。夜歌特殊變量有不確定的作用域和動態(tài)的范圍。
函數(shù)調(diào)用(symbol-value var),這里var是求值成一個符號,可以用來獲得一個特殊變量的當(dāng)前值。為了設(shè)置一個特殊變量,下面兩種形式是完全等同的:
(setf (symbol-value var) value)
(set var value)
Var和value會被求值。沒有訪問和設(shè)置詞法變量的對應(yīng)形式。特殊變量在符號和值之間設(shè)置一個映射來訪問運行中的程序。這不像詞法變量(或者所有傳統(tǒng)語言中的變量),符號(標(biāo)識符)只在程序被編譯的時候有意義。一旦程序運行,標(biāo)示符就會被編譯,也就不能再訪問變量。之后出現(xiàn)在一個詞法變量的作用域中的代碼可以引用變量。
【s】給定下面詞法變量a和特殊變量b的初始化,let形式的值是什么?
(setf a 'global-a)
(defvar *b* 'global-b)
(defun fn () *b*)
(let (( a 'local-a)
(*b* 'local-b) )
(list a *b* (fn) (symbol-value 'a) (symbol-value '*b*)))

3.18 多值

本書通篇都在講,函數(shù)返回的值。歷史上,Lisp設(shè)計就是每一個函數(shù)都會返回值,即使是那些看上去更像過程而不是函數(shù)的函數(shù)也會返回一個值。但是有時候我們想要函數(shù)返回不止一個信息。當(dāng)然,我們可以通過列表或者結(jié)構(gòu)來存儲信息,但是我們就要面臨定義結(jié)構(gòu),每一次都要構(gòu)建實例,還有解析實例等等麻煩的事情。看看函數(shù)round,它是一種可以將一個浮點數(shù)四舍五入成整數(shù)的函數(shù)。所以(round 5.1)的結(jié)果就是5。有時候,程序員會需要小數(shù)部分。函數(shù)round就可以返回兩個值,整數(shù)部分和小數(shù)部分:
> (round 5.1) => 5 .1
箭頭后面有兩個值是因為round就返回兩個值。大部分時候多個值是會被忽視的,僅僅使用第一個值。所以(* 2 (round 5.1))的結(jié)果就是10,就像round只是一個返回單值的函數(shù)一樣,如果你想要獲得多值,你就一定要使用特殊形式,multiple-value-bind:
(defun show-both (x)
?(multiple-value-bind (int rem)
? ? (round x)
? (format t "-f = -d + -f" x int rem)))
> (show-both 5.1)
5.1 = 5 + 0.1
你可以使用函數(shù)values來定義你自己的多值函數(shù),values會將他的參數(shù)返回成多個值:
> (values 1 2 3) => 1 2 3
多值是個不錯的方案,因為不需要的話他就不引人注目。大部分時候使用round都是需要他的單值的整數(shù)部分。如果round不適用多值,如果將兩個值打包成一個列表或者結(jié)構(gòu),之后會在一般情況下很難使用。
當(dāng)然不返回值也是可以的,比如(values)表達(dá)式,有些過程就是為了他的效果而設(shè)計,比如打印函數(shù)。例如,describe及定義成打印信息之后不返回值。
> (describe 'x) ``Symbol X is in the USER package. It has no value, definition or properties.但是當(dāng)任何不返回值的表達(dá)式是嵌套在一個會返回值的上下文里面,仍然遵守Lisp一個表達(dá)式一個返回值的規(guī)則,返回的是nil。在下面的例子,describe不返回值,但是之后list會要求一個值,獲取的是nil。> (list (describe 'x)) Symbol X is in AILP package. It has no value, definition or properties. (NIL)`

3.19 關(guān)于參數(shù)

Common Lisp給用戶在定義函數(shù)形式參數(shù)上賦予了很多靈活性,也就延伸到了函數(shù)接受的實際參數(shù)上。下面的程序是一個算數(shù)練習(xí)。他會問用戶一系列的n問題,沒一個問題都是測試算術(shù)運算符op。運算符的實際參數(shù)將會是隨機(jī)數(shù):
(defun math-quiz (op range n)
?"Ask the user a series of math problems."
? (dotimes (i n)
? ? (problem (random range) op (random range))))
(defun problem (x op y)
?"Ask a math problem, read a reply, and say if it is correct."
? (format t "-&How much is -d -a -d?" x op y)
? (if (eql (read) (funcall op x y?
? ? (princ "Correct!")
? ? (princ "Sorry, that's not right.")))
下面是運行的例子:
> (math-quiz '+ 100 2)
How much is 32 + 60? 92
Correct!
How much is 91 + 19? 100
Sorry, that's not right.
這個math-quiz函數(shù)的問題在于他要求用戶鍵入三個參數(shù):運算符,范圍和迭代的次數(shù)。用戶必須記住參數(shù)的順序,還要記住引用的運算符。而且這些都預(yù)先設(shè)定了用戶是會加法的!
Common Lisp提供了兩種方式來處理這個問題。第一,程序員可以定義參數(shù)是可選的,并且為這些參數(shù)設(shè)定默認(rèn)值。例如,在math-quiz函數(shù)中我們可以安排+運算作為默認(rèn)的運算符,100是默認(rèn)的數(shù)字范圍,還有10是默認(rèn)的迭代次數(shù)。
(defun math-quiz (&optional (op '+) (range 100) (n 10))
?"Ask the user a series of math problems."
? (dotimes (i n)
? ? (problem (random range) op (random range)))
現(xiàn)在,(math-quiz)的意思和(math-quiz ‘+ 100 10)是一樣的。如果一個可選的參數(shù)單獨出現(xiàn)而沒有默認(rèn)值設(shè)定的話,默認(rèn)值就是nil。可選參數(shù)是很方便的;然而,假如用戶想要設(shè)置這些參數(shù)怎么辦?都OK,只要顯示的輸入對應(yīng)位置就可以了(math-quiz ‘+ 100 5)
Common Lisp也允許參數(shù)是位置不想管的。這些關(guān)鍵字參數(shù)就是在函數(shù)調(diào)用中顯式命名的。當(dāng)有很多默認(rèn)參數(shù),都有了默認(rèn)值,想要設(shè)定特定的參數(shù)的值的是后就很有用了。我們可以這樣定義math-quiz:
(defun math-quiz (&key (op '+) (range 100) (n 10))
?"Ask the user a series of math problems."
? (dotimes (i n)
? ? (problem (random range) op (random range)))
現(xiàn)在來說 (math-quiz :n 5)和(math-quiz :op '+:n 5 :range 100)的意思就是一樣的了。關(guān)鍵字參數(shù)可以用參數(shù)的名字來定義,之前加上一個冒號,后面就是設(shè)定的值,這種關(guān)鍵字,值的對可以安排任何順序。
不僅僅用在參數(shù)列表中,冒號開頭的符號稱作關(guān)鍵字可以用在任何地方。Lisp中使用的術(shù)語關(guān)鍵字和IQ他語言中的關(guān)鍵字概念是不一樣的。例如,Pascal中的關(guān)鍵字(或者叫保留字)就是指愈發(fā)符號,if,else,begin,end等等。在Lisp中我們成這樣的符號叫做特殊形式操作符或者簡稱特殊形式。Lisp關(guān)鍵字就是存在在關(guān)鍵字包中的符號。包就是一張符號表,是字符串和命名符號之間的映射。這些關(guān)鍵字沒有特殊的意義,雖然他們是有一些不尋常的特性:關(guān)鍵字是常量,求值為自身,不像其他的符號,求值的結(jié)果是以符號命名的變量。關(guān)鍵字也剛好被用來定義&key參數(shù)列表,但是這是對他們的值的好處,不是對語法規(guī)則的效果。很重要的事情是關(guān)鍵字可以用在函數(shù)調(diào)用,但是一般的非關(guān)鍵字符號會用在函數(shù)定義的參數(shù)上。
由于歷史原因,令人困惑的一點就是符號&optional,&rest和&key被稱作lambda列表關(guān)鍵字。不像帶冒號的關(guān)鍵字,這種在lambda列表關(guān)鍵字中的&符號沒有特殊的含義,看一下這些標(biāo)記樣例:
> :xyz =? :XYZ ; keywords are self-evaluating
> &optional => ? ; lambda-list keywords are normal symbols
Error: the symbol &optional has no value
> '&optional => &OPTIONAL
> (defun f (&xyz) (+ &xyz &xyz)) => F ;&hasnosignificance
> (f 3) => 6
> (defun f (:xyz) (+ :xyz :xyz)) =>
Error: the keyword :xyz appears in a variable list.
Keywords are constants, and so cannot be used as names of variables.
? > (defun 9 (&key x y) (list x y)) => G
? > (let (( keys '(: x : y : z) ) ) ; keyword args can be computed
(g (second keys) 1 (first keys) 2)) => (2 1)
本章出現(xiàn)的很多函數(shù)都可以接受關(guān)鍵字參數(shù)來是的函數(shù)功能更加強大。例如,回憶一下find函數(shù),就可以用一個特定的元素來搜索序列。
> (find 3 '(I 2 3 4 -5 6.0)) => 3
實際上find是接受可選的關(guān)鍵字參數(shù)的:
> (find 6 '(I 2 3 4 -5 6.0)) => nil
之所以搜索不到是因為find使用的相等測試是eql,這樣子6和6.0是不相等的。然而,在equalp中6和6.0是相等的,所以我們使用test關(guān)鍵字:
> (find 6 '(I 2 3 4 -5 6.0) :test #'equalp) => 6.0
我們可以對test關(guān)鍵字使用任何二進(jìn)制斷言,例如,在序列中找找第一個大于4的數(shù)字:
> (find 4 '(I 2 3 4 -5 6.0) :test #'<) => 6.0
假設(shè)我們現(xiàn)在是不關(guān)心數(shù)字前的正負(fù)號;如果我們搜索5,我們想找到-5.我們可以用key關(guān)鍵字來接受絕對值函數(shù)來操作每一個元素:
> (find 5 '(1 234 -5 6.0) :key #'abs) => -5
關(guān)鍵字參數(shù)極大地擴(kuò)展了內(nèi)建函數(shù)的可用性,斌企鵝他們可以對你自己定義的函數(shù)實現(xiàn)一樣的功能。在內(nèi)建函數(shù)中,最常用的關(guān)鍵字一般分為兩類:test,test-not和key,是用在匹配函數(shù)上,還有就是start,end和from-end是用在序列函數(shù)中的。一些函數(shù)接受關(guān)鍵字的集合。(CLTL不鼓勵使用test-not關(guān)鍵字,雖然這個關(guān)鍵字還是語言的一部分)。
匹配函數(shù)包括了sublis, position, subst, union, intersection, set-difference, remove, remove-if, subsetp, assoc, find, 還有member。他們默認(rèn)的相等測試都是eql。這個選項可以使用關(guān)鍵字參數(shù)test來更改,或者反過來用test-not定義。另外,比較的過程可以和對象的部分進(jìn)行,不必和整個對象進(jìn)行,只要用選擇器函數(shù)用key參數(shù)定義就可以。
序列函數(shù)包括了remove,remove-if,position和find。最常用的序列類型就是列表,但是字符串和向量也可以看做是序列。一個序列函數(shù)會對序列中的元素反腐之星一些操作。默認(rèn)的順序就是從序列的頭遍歷到尾,也可以設(shè)置反過來的順序,要使用from-end關(guān)鍵字,參數(shù)是t,也可以定義一個子序列,只要使用關(guān)鍵字start和end來接受數(shù)字就可以。序列的第一個元素的索引是0,不是1,所以要小心一些。
舉個例子,結(jié)社我們要寫一個類似于find或者find-if函數(shù)的序列函數(shù),只是他返回的是所有匹配元素的列表而不僅僅是匹配的第一個元素。我們叫新的函數(shù)是find-all和find-all-if。另一種方式是,將函數(shù)看做是remove函數(shù)的變種,將所有匹配的元素留下,不匹配的元素移除。從這個觀點看,我們可以看到函數(shù)find-all-if的功能實際上是和remove-if-not一樣的。有時候同一個函數(shù)有兩個名字是很有用的(比如not和null)。新的名字可以用defun來定義,但是拷貝定義是個更加方便的做法:
(setf (symbol-function 'find-all-if) #'remove-if-not)
遺憾的是,沒有一個內(nèi)建函數(shù)和find-all函數(shù)嚴(yán)格對應(yīng),所以我們不得不親手定義一個。還好,remove可以完成很多工作。我們要做的就是安排跳過滿足test的斷言的元素。例如,檢索列表中所有等于1的元素就是和移除不等于1的元素師一樣的。
> (setf nums '(1 2 3 2 1)) => (1 2 3 2 1)
? > (find-all 1 nums :test #'=) == (remove 1 nums :test #'/=) => (1 1)
現(xiàn)在我們需要的是一個高等級的函數(shù),可以返回一個函數(shù)的補集部分,換句話說,給定等于,我們想要不等于的部分,這樣的函數(shù)在ANSI Common Lisp中稱作complement,但是在早期的版本中沒有定義,所以在這里給出:
(defun complement (fn)
"If FN returns y, then (complement FN) returns (not y)."
;; This function is built-in in ANSI Common Lisp,
;; but is defined here for those with non-ANSI compilers.
#'(lambda (&rest args) (not (apply fn args))))
當(dāng)使用給定的test斷言來調(diào)用find-all的時候,我們做的就是小勇remove來移除test斷言的補集部分。這個方法在沒有test參數(shù)部分也是成立的,因為默認(rèn)就是eql。還有一個需要測試的情況就是制定test-not斷言的時候,它的參數(shù)是反過來的。當(dāng)test和test-not制定了同一個參數(shù)的時候,就會報錯,所以我們不需要測試這個情況:
(defun find-all (item sequence &rest keyword-args
? ? ? ?&key (test #'eql) test-not &allow-other-keys)
?"Find all those elements of sequence that match item,
?according to the keywords. Doesn't alter sequence."
?(if test-not
? ?(apply #'remove item sequence
? ? ?:test-not (complement test-not) keyword-args)
? ?(apply #'remove item sequence
? ? ?:test (complement test) keyword-args)))
這個定義唯一的難點就是理解參數(shù)列表。&rest會收集在變量關(guān)鍵字參數(shù)中所有的鍵值對。除了rest參數(shù),還有兩個特定的關(guān)鍵字參數(shù),test和test-not。任何時候在參數(shù)列表中有key關(guān)鍵字的話,就需要一個allow-other-keys,如果其他參數(shù)允許的話。在這個情況下我們想要接受關(guān)鍵字,start和key然后傳遞參數(shù)給remove。
所有的鍵值對都會累加緊關(guān)鍵字參數(shù)的列表中,包括test或者test-not的值。所以我們就有了:
(find-all 1 nums :test #'= :key #'abs)
== (remove 1 nums :test (complement #'=) :test #'= :key #'abs)
(1 1)
請注意對于remove的調(diào)用包含了兩個test關(guān)鍵字,這不是個錯誤,Common Lisp聲明最左邊的值是計數(shù)的那個。
【s】你認(rèn)為為什么是兩個之中最左邊的那個起作用而不是右邊那個?

3.20 Lisp還有的其他部分

Common Lisp的內(nèi)容比我們在這一章看到的多的多,但是這個概覽對于應(yīng)付下面的章節(jié)應(yīng)該是足夠了,嚴(yán)謹(jǐn)?shù)腖isp程序員應(yīng)該認(rèn)真的學(xué)習(xí)參考書籍或者是在線文檔。在第五部分或許你會發(fā)現(xiàn)很有用,特別是第24章,介紹了一些Common Lisp的高級特性(比如包和錯誤處理)還有第25章,是對有疑惑的Lisp程序員的一個問題解決參考。
再繼續(xù)介紹函數(shù)的細(xì)節(jié)的話會分散本書的主體,打斷AI程序的描述不是本書的本意,畢竟AI編程才是本書的主題。
(第一部分 完)

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

推薦閱讀更多精彩內(nèi)容