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

3.4 說說相等和內部表示

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


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

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


相等斷言比較.JPG

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

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

3.6 維護表格的函數

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

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

3.7 操作樹的函數

很多Common Lisp函數將表達式((a b) ((c)) (d e))看做一個三元素的序列,但是也有一些函數會把他看做一個有5個非空葉子節點的樹。函數copy-tree會創建一個樹的拷貝,函數tree-equal會測試兩個樹在組合單元層面是不是相等。這這方面tree-equal和equal相似,但是tree-equal更加強大,因為他允許:test關鍵字:
>(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))在組合單元層面的表現

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

還有兩個函數是將原來舊的表達式用一個新的表達式替換。Subst會替換一個簡單的值,而sublis會用聯合列表的形式(old . new)對,來替換列表。請注意在alist中舊值和新值的順序,sublis和subst的參數順序是反的。名字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 操作數字的函數

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

表達式 計算結果 功能含義
(+ 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 的隨機數
(expt 4 2) =>16 求冪(還有exp和log函數)
(sin pi) =>0.0 sin函數(還有cos,tan等等)
(asin 0) =>0.0 sin的反函數arcsin(還有acos,atan等等)
(min 2 3 4) =>2 最小數(還有max)
(abs -3) =>3 絕對值
(sqrt 4) =>4 平方根
(round 4.1) =>4 化為整數(還有truncate,floor,ceiling)
(rem 11 5) =>1 余數(還有mod)
3.9 操作集合的函數

列表的重要用處之一就是表示集合。Common Lisp提供了函數來這樣操作列表。例如,一般表現集合的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)
有些實現會返回(C D)作為答案,另外一些實現會返回(D C)。相同的集合,都是合法的,你的程序也不應該依賴結果的元素順序。下面是一些操作集合的主要函數

表達式 計算結果 功能含義
(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 子集關系判斷
(adjoin ‘b s) =>(b c d e) 給集合加一個元素
(adjoin ‘c s) =>(c d e) 加一個元素,但是不加重復元素

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

處理列表的函數 處理整型數的函數 處理比特向量的函數
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 具有破壞性的函數

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

3.11 數據類型概覽

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

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

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

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

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

3.12 輸入輸出I/O

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

3.13 調試工具

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

知道對象名字之后,describe函數就可以用來獲取進一步的信息
> (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
如果只是想要符號的文檔字符串,函數documentation會取得:
> (documentation 'first 'function) => "Return the first element of LIST."
> (documentation 'pi 'variable) => "pi"
如果你想查看或者更改復雜結構的租金啊,工具inspect可以使用。在一些實現中,他會調用一個基于窗口的瀏覽器。
Common Lisp也提供一個在出錯的時候自動進入的調試器,錯誤或許是無意的或許是語義的深層錯誤,都會自動進入調試器。調試器的細節根據實現不同而不同,但是進入調試器的方法是有標準的。函數break會進入調試器然后打印條可選的信息。這是有意設置成為調試斷點的主要方法。Break只為調試目的服務;當一個程序被認為是可以運行,所有對break的調用都應該移除。然而,使用函數error,cerror,assert,或者chek-type做一個非常條件下的檢查或許是個好主意,這些函數我們在之后會介紹。

3.14 防錯工具

在代碼中包含防錯檢查是除了正常調試外的一個很好地做法。防錯代碼會檢測錯誤并且可能會做出正確的操作。
函數error和cerror就是用來在出錯的時候發出信號。這些函數調用即使是在調試之后也會保留。函數error接受一耳光格式化字符串和一些可選參數。如果發出的是致命錯誤信號,程序就會停止運行,并且不讓用戶再啟動了。例如:
(defun average (nubers)
?(if (null numbers)
? ?(error “Average of the ampty list is undefined.”)
? ?(/ (reduce #’+ numbers)
? ? ?(length numbers))))
在很多情況下,致命錯誤是很猛的。還有一個函數cerror是可繼續錯誤的縮寫。Cerror接受兩個格式化字符串;第一個會打印信息,顯示如果我們繼續會發生什么,第二個打印錯誤信息本身。Cerror實際上不會做任何修正錯誤的操作,只不過允許用戶發出認可錯誤繼續運行的信號罷了。在下面的實現中,用戶通過鍵入:continue來繼續運行。在ANSI Common Lisp中,還有其他的方式來制定繼續的選項。
(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
在這個例子中如果加入錯誤檢查的話,會讓代碼的長度倍增。一般不會這么做,這也是工作在給定輸入的代碼和工作在所有錯誤環境下的代碼的一個重大區別。Common Lisp嘗試使用一個實現一些特殊形式來提供錯誤檢查。Ecase形式就是error case的縮寫,他就像一個正常的case形式,除了一點,要是沒有情況滿足條件,就會報錯。還有ccase是continuable case的縮寫。和ecase一樣,除了錯誤是可繼續的。系統要求測試對象有一個新的值,知道說用戶支持了匹配的對象之一。
為了讓錯誤檢查不會帶來代碼規模的膨脹,Common Lisp提供了特殊形式check-type和assert。如其名,check-type用來檢查參數類型。如果參數的類型錯誤,就會報出一個可繼續錯誤。例如:
(defun sqr (x)
?“Multiply x by itself.”
?(check-type x number)
?(* x x))
如果sqr的參數不是一個數字的話就對報出一個適當的錯誤信息:
> (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測試一個表達式,根絕返回值的真假來報出信號:
(defun sqr (x)
?"Multiply x by itself."
?(assert (numberp x))
?(* x x))
出現這種斷言想再繼續是不可能的了。但是可以給assert一系列的可修改的參數,嘗試使得assert的返回值為真。下面的例子中,變量x就是唯一一個可以改變的:
(defun sqr (x)
?"Multiply x by itself."
?(assert (numberp x) (x))
? (* x x))
如果違法了這個斷言,就會打印出一個錯誤信息,用戶會被給與一個可繼續的選項來更改x。如果x的值滿足了斷言,程序就會繼續,assert的返回值永遠是nil。
最后,對于想要更多的叢植錯誤信息的用戶可以提供一個格式控制字符串和可選選項。所以最復雜的assert語法是:
(assert測試形式部分 (位置…) 格式化控制字符串 格式化參數…)
這里是另一個例子。程序執行前斷言檢測,熊喝的麥片粥是不是太燙了還是太冷了。
(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)))
在下面的交互過程中,斷言失敗了,打印了程序的錯誤信息,還有兩個可以繼續的可能選項。用戶選擇一個,調用make-porridge輸入一個新的值,函數就成功繼續了。
> (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總是層出不窮,花在排錯上的時間還不如寫斷言來的省時省力。
無論何時,擬開發了一個復雜的數據結構,比如某種數據庫,開發一個對應的一致性檢查器總是一個好的做法。一致性檢查就是看看整個數據結構測試所有可能的錯誤。當發現了一個新的錯誤,這個錯誤的檢查就應該成為一致性檢查的一部分了。調用一致性檢查是最快的幫助定位數據結構中bug的方式。
另外,保持進行一些測試也是很好的作法,當程序更改之后,很容易將之前排出的bug重新引回程序中。這種發福測試叫做回歸測試,Waters在1991年詐尸了一個維護回歸測試的工具集。但是也是很簡單的,可以使用一系列assert調用來委會一個非常規測試。
(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 表達式)可以用來看看執行這個表達式用了多少時間,一些實現也會打印靜態的內存占用空間:
> (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倍,而且用的空間更少。大部分嚴謹的Common Lisp程序員都會使用編譯版本的程序工作。然而一般看來,在開發程序的伊始就考慮效率問題是不大合適的。更好的是設計一個靈活性比較高的程序,先運行起來,之后在修改的更加高效。換句話說,就是講開發階段和優化階段分開。第9章和第10章會給出提高效率的更多細節,第25章會給出更多關于調試和排錯技術的建議。

3.15 求值

Lisp中有三個函數是做求值的:funcall,apply和eval。Funcall用于吧一個函數應用在獨立的函數上,apply是用于將一個函數應用到參數的列表上。實際上apply可以再最后的參數之前有一個或者多個獨立的具有完整形式的參數,函數或者特殊形式,之后又參數或者原子。下面的五個語句是相等的:
> (+ 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靈活性的關鍵所在。在現代Lisp版本中,使用靜態域的版本,比如Common Lisp,eval的使用越來越少(事實上,Scheme中就沒有eval)。代之的是,程序員虎使用lambda表達式創建一個新函數,之后用apply或者funcall來調用函數。一般來說,如果你發現你正在使用eval,那一般就是你做錯了。

3.16 閉包

創建一個新函數的意思到底是什么?當然每一次一個特殊形式function或者井號單引號的簡略形式被求值的話,一個函數就會被反悔。但是在這個例子中我們看到,返回的函數總是相同的。
> (mapcar #'(lambda (x) (+ x x)) '(1 3 10)) => (2 6 20)
每一次我們對lambda表達式求值,返回的函數就會對后面的參數進行加倍操作。然而在一般情況下,一個函數是由函數的主體和函數像伴隨的函數引用自由詞域變量組成的。這樣的一個組合叫做詞法閉包,或者簡稱閉包,因為語法變量是閉包在函數中的。看下面的例子:
(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不同的值來調用adder,他都會創建一個不同的函數,函數會把c加到它的參數上。既然每一次對adder的調用都創建了一個不同的本地變量c,那么每一次adder返回的函數也會是一個不一樣的函數。下面是另一個例子,函數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 的調用創建了兩個不同的閉包,每一個詞法變量balance都有不同的值。隨后對兩個閉包的調用分別改變了它們的變量的值,但是兩個賬戶之間是沒有混淆的。
> (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
這種編程風格會在第13章有更加詳細的介紹。

3.17 特殊變量

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

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

3.19 關于參數

Common Lisp給用戶在定義函數形式參數上賦予了很多靈活性,也就延伸到了函數接受的實際參數上。下面的程序是一個算數練習。他會問用戶一系列的n問題,沒一個問題都是測試算術運算符op。運算符的實際參數將會是隨機數:
(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函數的問題在于他要求用戶鍵入三個參數:運算符,范圍和迭代的次數。用戶必須記住參數的順序,還要記住引用的運算符。而且這些都預先設定了用戶是會加法的!
Common Lisp提供了兩種方式來處理這個問題。第一,程序員可以定義參數是可選的,并且為這些參數設定默認值。例如,在math-quiz函數中我們可以安排+運算作為默認的運算符,100是默認的數字范圍,還有10是默認的迭代次數。
(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)))
現在,(math-quiz)的意思和(math-quiz ‘+ 100 10)是一樣的。如果一個可選的參數單獨出現而沒有默認值設定的話,默認值就是nil。可選參數是很方便的;然而,假如用戶想要設置這些參數怎么辦?都OK,只要顯示的輸入對應位置就可以了(math-quiz ‘+ 100 5)
Common Lisp也允許參數是位置不想管的。這些關鍵字參數就是在函數調用中顯式命名的。當有很多默認參數,都有了默認值,想要設定特定的參數的值的是后就很有用了。我們可以這樣定義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)))
現在來說 (math-quiz :n 5)和(math-quiz :op '+:n 5 :range 100)的意思就是一樣的了。關鍵字參數可以用參數的名字來定義,之前加上一個冒號,后面就是設定的值,這種關鍵字,值的對可以安排任何順序。
不僅僅用在參數列表中,冒號開頭的符號稱作關鍵字可以用在任何地方。Lisp中使用的術語關鍵字和IQ他語言中的關鍵字概念是不一樣的。例如,Pascal中的關鍵字(或者叫保留字)就是指愈發符號,if,else,begin,end等等。在Lisp中我們成這樣的符號叫做特殊形式操作符或者簡稱特殊形式。Lisp關鍵字就是存在在關鍵字包中的符號。包就是一張符號表,是字符串和命名符號之間的映射。這些關鍵字沒有特殊的意義,雖然他們是有一些不尋常的特性:關鍵字是常量,求值為自身,不像其他的符號,求值的結果是以符號命名的變量。關鍵字也剛好被用來定義&key參數列表,但是這是對他們的值的好處,不是對語法規則的效果。很重要的事情是關鍵字可以用在函數調用,但是一般的非關鍵字符號會用在函數定義的參數上。
由于歷史原因,令人困惑的一點就是符號&optional,&rest和&key被稱作lambda列表關鍵字。不像帶冒號的關鍵字,這種在lambda列表關鍵字中的&符號沒有特殊的含義,看一下這些標記樣例:
> :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)
本章出現的很多函數都可以接受關鍵字參數來是的函數功能更加強大。例如,回憶一下find函數,就可以用一個特定的元素來搜索序列。
> (find 3 '(I 2 3 4 -5 6.0)) => 3
實際上find是接受可選的關鍵字參數的:
> (find 6 '(I 2 3 4 -5 6.0)) => nil
之所以搜索不到是因為find使用的相等測試是eql,這樣子6和6.0是不相等的。然而,在equalp中6和6.0是相等的,所以我們使用test關鍵字:
> (find 6 '(I 2 3 4 -5 6.0) :test #'equalp) => 6.0
我們可以對test關鍵字使用任何二進制斷言,例如,在序列中找找第一個大于4的數字:
> (find 4 '(I 2 3 4 -5 6.0) :test #'<) => 6.0
假設我們現在是不關心數字前的正負號;如果我們搜索5,我們想找到-5.我們可以用key關鍵字來接受絕對值函數來操作每一個元素:
> (find 5 '(1 234 -5 6.0) :key #'abs) => -5
關鍵字參數極大地擴展了內建函數的可用性,斌企鵝他們可以對你自己定義的函數實現一樣的功能。在內建函數中,最常用的關鍵字一般分為兩類:test,test-not和key,是用在匹配函數上,還有就是start,end和from-end是用在序列函數中的。一些函數接受關鍵字的集合。(CLTL不鼓勵使用test-not關鍵字,雖然這個關鍵字還是語言的一部分)。
匹配函數包括了sublis, position, subst, union, intersection, set-difference, remove, remove-if, subsetp, assoc, find, 還有member。他們默認的相等測試都是eql。這個選項可以使用關鍵字參數test來更改,或者反過來用test-not定義。另外,比較的過程可以和對象的部分進行,不必和整個對象進行,只要用選擇器函數用key參數定義就可以。
序列函數包括了remove,remove-if,position和find。最常用的序列類型就是列表,但是字符串和向量也可以看做是序列。一個序列函數會對序列中的元素反腐之星一些操作。默認的順序就是從序列的頭遍歷到尾,也可以設置反過來的順序,要使用from-end關鍵字,參數是t,也可以定義一個子序列,只要使用關鍵字start和end來接受數字就可以。序列的第一個元素的索引是0,不是1,所以要小心一些。
舉個例子,結社我們要寫一個類似于find或者find-if函數的序列函數,只是他返回的是所有匹配元素的列表而不僅僅是匹配的第一個元素。我們叫新的函數是find-all和find-all-if。另一種方式是,將函數看做是remove函數的變種,將所有匹配的元素留下,不匹配的元素移除。從這個觀點看,我們可以看到函數find-all-if的功能實際上是和remove-if-not一樣的。有時候同一個函數有兩個名字是很有用的(比如not和null)。新的名字可以用defun來定義,但是拷貝定義是個更加方便的做法:
(setf (symbol-function 'find-all-if) #'remove-if-not)
遺憾的是,沒有一個內建函數和find-all函數嚴格對應,所以我們不得不親手定義一個。還好,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)
現在我們需要的是一個高等級的函數,可以返回一個函數的補集部分,換句話說,給定等于,我們想要不等于的部分,這樣的函數在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))))
當使用給定的test斷言來調用find-all的時候,我們做的就是小勇remove來移除test斷言的補集部分。這個方法在沒有test參數部分也是成立的,因為默認就是eql。還有一個需要測試的情況就是制定test-not斷言的時候,它的參數是反過來的。當test和test-not制定了同一個參數的時候,就會報錯,所以我們不需要測試這個情況:
(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)))
這個定義唯一的難點就是理解參數列表。&rest會收集在變量關鍵字參數中所有的鍵值對。除了rest參數,還有兩個特定的關鍵字參數,test和test-not。任何時候在參數列表中有key關鍵字的話,就需要一個allow-other-keys,如果其他參數允許的話。在這個情況下我們想要接受關鍵字,start和key然后傳遞參數給remove。
所有的鍵值對都會累加緊關鍵字參數的列表中,包括test或者test-not的值。所以我們就有了:
(find-all 1 nums :test #'= :key #'abs)
== (remove 1 nums :test (complement #'=) :test #'= :key #'abs)
(1 1)
請注意對于remove的調用包含了兩個test關鍵字,這不是個錯誤,Common Lisp聲明最左邊的值是計數的那個。
【s】你認為為什么是兩個之中最左邊的那個起作用而不是右邊那個?

3.20 Lisp還有的其他部分

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370

推薦閱讀更多精彩內容