第3章 Lisp概覽
毫無疑問,Common Lisp是一門龐大的語言。——Guy L. Steele(另一本Lisp書籍的作者)
本章簡要地概括了Lisp中最重要的特殊形式和函數(shù)。有經(jīng)驗的Common Lisp程序員可以隨便看看或者跳過,但是對于初學者或者初次接觸Common Lisp這個方言的讀者是一定要認真研讀的。
本章可以作為一個參考手冊使用,更加權威的參考手冊是CLTL第二版。CLTL的長度是本書的25倍,很明顯我們在這里只能提點一下要領。更多的細節(jié)之后會在書中的程序里說明。
3.1 Lisp風格指南
一開始Common Lisp程序員經(jīng)常被語言提供的眾多選擇嚇到。在本章我們展示了十四個不同的方式來求出列表的長度。那怎么從中選擇一個?答案之一就是參考優(yōu)秀的程序,并且模仿他們的風格。一般來說,每一個程序員都應該謹記留個準則:
要具體明確一些
使用抽象機制
要簡明精煉一些
用已提供的工具
不要隱匿晦澀
前后一致
使用盡可能具體的形式可以讓你的代碼的讀者明白了解你的意圖,例如條件式中when就比if更清晰。讀者一看到when就會想到,接下來的測試部分是不是為真。見到if就會想起兩個部分then和else。If只是用一個部分也是可以的,傾向于使用when的原因是when更加清晰。
使得具體的重要方式就是使用抽象。Lisp提供的數(shù)據(jù)結構很樸實,比如列表和數(shù)組。他們可以用來實現(xiàn)你需要的數(shù)據(jù)結構,但是你不應該直接調用原始函數(shù),如果你這樣定義給一個名字列表:
(defvar *names* '((Robert E. Lee) . ..))
你就應該定義獲得這個列表每一個組件的函數(shù)。獲得Lee,應該使用(last-name (first names)),而不是(caddar names)。
上面的幾個準則之間,常常是互相融合的。比如說,你的代碼是在列表中尋找一個元素,你應該使用find(或者find-if),而不是loop或者do。Find比一般的指令loop或者do更加明晰,這就是一種抽象,更加精巧,find是一個內(nèi)建工具,很容易理解。
有時候原則之間是互相沖突的,經(jīng)驗會啟示你應該傾向于哪一個原則比較好。看看接下來的,將一對新的鍵值放進一個聯(lián)合列表(聯(lián)合列表會在3.6節(jié)介紹)中的兩種方式:
(push (cons key val) a-list)
(setf a-list (acons key val a-list))
第一種方法顯得更加精簡。但是第二種更加明晰,他用的acons函數(shù)就是專門為聯(lián)合列表所設計的。這時候選擇就變得很糾結,熟悉acons函數(shù)的可能傾向于第二種,而喜歡簡潔代碼的人則會用第一種。
相似的糾結會在給變量賦值的時候出現(xiàn)。一些人喜歡用(setq x val),因為這樣最明晰具體;其他人會用(setf x val),這樣子可以保持前后的一致,使用setf來進行所有的更新操作。無論你選擇哪一種,請記住第六條原則:前后一致。
3.2 特殊形式
第一章說過,書中所說的特殊形式,既是指Common Lisp的語法結構,也是說這些結構中的保留關鍵字。使用的最普遍的關鍵字是:
定義用 | 條件式 | 變量 | 循環(huán)迭代 | 其他 |
---|---|---|---|---|
defun | and | let | do | declare |
defstruct | case | let* | do* | function |
defvar | cond | pop | dolist | progn |
defparameter | if | push | dotimes | quote |
defconstant | or | setf | loop | return |
defmacro | unless | incf | trace | |
labels | when | decf | untraced |
其實只有declare,function,if,labels,let,let星,progn和quote是真正的特殊形式。其他的都是用宏的方式調用原始函數(shù)和特殊形式定義的。對于程序員來說,這沒有區(qū)別,Common Lisp實現(xiàn)是自由轉換宏和特殊形式的實現(xiàn)的,反之亦然,所以出于簡單一直,我們會繼續(xù)使用特殊形式這一個術語來統(tǒng)一稱呼真正的特殊形式和內(nèi)建宏。
用于定義的特殊形式
本節(jié)我們研究用于定義的特殊形式,會被用在定義全局函數(shù),宏,變量和結構。之前已經(jīng)用過defun了;defmarco和defun相似,在后面介紹。
(defun 函數(shù)名 (參數(shù)。。。) “可選的文檔字符串” 主體)
(defmacro 宏名 (參數(shù)。。。) “可選的文檔字符串” 主體)
引入特殊變量的形式有三個。Defvar定義一個特殊變量并且可以支持賦予一個初始值和一個文檔字符串。初始值的求值和賦值僅僅是在變量沒有任何值的時候。Defparameter是相似的,除了它的值是必須要有的,也可以用在更改任何已存在的變量值。Defconstant被用在聲明一個符號,來代表一個特定的值。
(defvar 變量名 初始值 “可選的文檔字符串”)
(defparameter 變量名 初始值 “可選的文檔字符串”)
(defconstant 變量名 初始值 “可選的文檔字符串”)
所有def開頭的形式定義的是全局的對象。定義本地變量可以使用let,定義本地函數(shù)會使用labels,之后我們再看。
大部分的編程語言提供一個組織相關對象帶一個結構中的方式。Common Lisp也不例外。特殊形式defstruct就是定義一個結構類型(Pascal中叫做record類型),并且會自動定義取得結構組件的函數(shù),一般的語法是這樣:
(defstruct 結構名 “可選的文檔字符串” 位置)
我們舉個例子,為名字定義一個結構:
(defstruct name
First
(middle nil)
last)
自動定義的結構器函數(shù)叫做make-name,判定斷言(就是判斷是不是這個類型)叫做name-p,用于訪問的函數(shù)佳作name-first,name-middle和name-last。(middle nil)意思是每一個由make-name創(chuàng)建的新名字會有一個默認的中間名nil。接下來我們來創(chuàng)建,訪問和修改一個結構:
> (setf b (make-name :first 'Barney :last 'Rubble)) =>
#S(NAME :FIRST BARNEY : LAST RUBBLE)
?> (name-first b) ?=> BARNEY
> (name-middle b) => NIL
> (name-last b) => RUBBLE
> (name-p b) => T
> (name-p 'Barney) =>? NIL ;only the results of make-name are names
?> (setf (name-middle b) '0) => 0
?> b => #S(NAME :FIRST BARNEY :MIDDLE 0 :LAST RUBBLE)
一個結構的打印形式是用一個符號井號S開始的,之后就是結構類型和可選的鍵值對組成的列表。別被這個形式給迷惑了:這是一個結構的打印形式,但不是說內(nèi)部結構的表現(xiàn)也是這樣,結構的實現(xiàn)實際上更像向量。對于結構name,這個類型將會是零元素的想想,第一個元素是名字,第二個元素是中間名,第三個是姓。這意味著結構比列表更加高效:節(jié)省了很多空間,任何元素都可以用簡單的步驟進行訪問。在一個列表中,訪問第n個元素需要n個步驟。
還有一些結構會有更多的控制手段,并有獨立的鍵值位置。等出現(xiàn)的時候我們再介紹。
用于條件式的特殊形式
我們用的這類特殊形式有if,就是選擇的分支結構。在Lisp中,只有nil會在if的測試部分被當做false來看,其他的都是真。但傳統(tǒng)上的慣例是將t作為正統(tǒng)的真值表示。
很少有特殊形式是做條件求值的,從技術阿的角度上說,if是定義成特殊形式的,其他的條件式去卻都是用宏實現(xiàn)的,某種程度上說if是最最基礎的。有些程序猿傾向于用if,也有的人傾向于最長最豐富的功能而用cond。最終的選擇跟自然語言中差不多,隨你高興,自由地使用when,unless,if或者其他的,隨便選一個。
下面的表是告訴我們,實際上任何一種條件式都可以用if和cond來實現(xiàn)。實際上這些實現(xiàn)是不準確的,因為or,case和cond不會對任何表達式求值超過一次,但是使用if的實現(xiàn)對一些表達式進行了多次求值。這張表也有對cond的實現(xiàn)。Cond的語法是一系列的條件語句,每一個都是由一個測試表達式和一定數(shù)量的結果表達式組成的。
(cond (測試表達式 結果表達式…)
?(測試表達式 結果表達式…)
?…)
Cond會依次讀取條件從句,對每一個測試表達式求值。如果測試表達式的結果為真,那么就會對每一個結果表達式求值,返回最后一個表達式的值作為cond的值。如果一個條件從句只有測試表達式?jīng)]有結果表達式,那么就會返回測試表達式本身。如果所有的測試表達式都是假,那么就會返回nil作為cond的返回值。一般來說,cond的最后一個條件從句會設置成(t 結果表達式),這樣的形式就確保肯定會有一個結果值被返回。
When和unless的形式就像cond的一個單獨的條件從句,如果測試部分滿足就進行求值,后面跟的是任意數(shù)量的語句序列。
And測試的是每一個語句都是不是為真,or測試的是是不是有一個為真。求值順序都是從左向右的,只要是有條件滿足,就會停止求值。也就是說,and求出有一個為假,or求出一個真,就會停止求值了。
條件式 | if形式 | cond形式 |
---|---|---|
(when test a b c) | (if test (prong a b c)) | (cond (test a b c)) |
(unless test x y) | (if (not test) (prong x y)) | (cond ((not test) x y)) |
(and a b c) | (if a (if b c)) | (cond (a (cond (b c)))) |
(or a b c ) | (if a a (if b b c)) | (cond (a) (b) (c)) |
(case a (b c) (t x)) | (if (eql a ‘b) c x) | (cond ((eql a ‘b) c) (t x)) |
And和or除了在測試邏輯條件式之外的應用都是不合適的。When,unless和if都可以用來做條件判斷。
(and (> n 100)
?(princ "N is large.")) ;Bad style!
(or <= n 100)
? (princ "N is large.")); Even worse style!
(cond ((> n 100) ;OK, but not MY preference
? (princ "N is large."))
?(when (> n 100)
? (princ "N is large.")) ; Good style.
如果主要目的是返回一個值,而不是做什么操作的話,cond和if會比when和unless更加實用。When和unless是隱式返回nil,當只有一種可能性存在的話,when和unless就比if要好,if有兩種分支,cond則是由多種分支。
(defun tax-bracket (income)
?“Determine what percent tax should be paid for this income.”
?(cond ((< income 10000.00) 0.00)
? ?((< income 30000.00) 0.20)
? ?((< income 50000.00) 0.25)
? ?((< income 70000.00) 0.30)
? ? (t 0.35)))
如果測試部分是要和一個常量比較的話,case會比較合適。
(case 表達式
(匹配 結果…) …)
對表達式求值過后會和接下來每一個匹配進行比較。只要有一個是eql相等的,結果表達式就會被求值,最后一個值就會返回。請注意匹配是不求值的。如果一個匹配表達是一個列表的話,case就會把表達式的值和列表中的每一個元素進行比較,看看是不是eql相等。如果匹配部分是一個符號otherwise(或者是符號t),就會默認執(zhí)行。也就是說otherwise會放在最后一個匹配位置。
還有另一個特殊形式,typecase,會將表達式的類型和幾種分支進行比較,和case類似,會選擇第一個匹配條件從句進行計算。另外,特殊形式ecase和etypecase也是和case,typecase差不多的,只不過在沒有匹配存在的情況下會發(fā)出錯誤信號。你可以吧e看成是exhaustive或者是error。Ccase和ctypecase也會發(fā)出錯誤,但是是可持續(xù)的錯誤(也就是致命錯誤):
(case x
?(1 10)
? (2 20))
(cond
? ((eql x 1) 10)
? ((eql x 2) 20))
(typecase x
? (number (abs x))
? (list (length x)))
(cond
? ((typep x ‘number) (abs x))
? ((typep x ‘list) (length x)))
(ecase x
? (1 10)
? (2 20))
(cond
? ((eql x 1) 10)
? ((eql x 2) 20)
? (t (error “no valid case”))
(etypecase x
? (number (abs x))
? (list (length x)))
(cond
? ((typep x ‘number) (abs x))
? ((typep x ‘list) (length x))
? (t (error “no valid typecase”)))
處理變量和位置的特殊形式
Setf一般是用在給變量或者位置賦予一個新的值,在其他語言中一般是用=或者:=來做這個功能。一個位置,或者說普通變量就是一個值存儲的位置的名字。下面的這張表是Lisp的賦值語句和對應的Pascal形式:
;;Lisp | Pascal |
---|---|
(setf x 0) | x:=0; |
(setf (aref A i j) 0) | A[i,j] := 0; |
(setf (rest list) nil) | list^.rest := nil; |
(setf (name-middle b) ‘Q) | b^.middle := “Q”; |
Setf可以用來設置變量,設置結構中的組件。像Pascal語言中,出現(xiàn)在賦值語句左半邊的表達式是被語言的語法做了嚴格限定的。在Lisp中,用戶可以使用特殊形式defsetf或者define-setf-method來擴展setf形式。這個會在之后特別介紹。
也有一些內(nèi)建的函數(shù)來更改位置。例如,(rplacd list nil)就和表達式(setf (rest list) nil)的效果一樣,除了返回值不一樣之外。Common Lisp的程序員大多傾向于使用setf形式而不是具體定義的函數(shù)。
如果僅僅是設置變量,特殊形式setq也可以使用。本書中我們?yōu)榱饲昂笠恢聲恢笔褂胹etf。
本節(jié)討論的事情看上去是說變量(還有結構中的鍵值)被賦予新的值的事情。實際上很多Lisp程序是不會有賦值操作的。使用函數(shù)式編程的話,經(jīng)常會引入新的變量,斗牛士一旦創(chuàng)建,基本上就不會再改變了。引入新變量的一個方式是作為函數(shù)的參數(shù)。也可以使用特殊形式let來引入本地變量。下面就是let的一般形式,每月各變量都綁定了對應的值,之后主體才會求值。
(let ((變量名 值)…)
?主體…)
(let ((x 40)
?? (y (+ 1 1)))
? (+ x y))
使用let定義本地變量和使用匿名函數(shù)定義參數(shù)是沒有區(qū)別的:
((lambda (變量…)
?主體…)
?參數(shù)值…)
((lambda (x y)
? (+ x y))
40
(+ 1 1))
首先,所有的值都會被求值。之后會綁定到每一個變量(lambda表達式的參數(shù)),最后主體會使用那些變量求值。
特殊形式let星,和let差不多,但是在綁定變量的時候會允許使用之前新定義的變量。
(let* ((x 6)
? ?(y (* x x)))
?(+ x y))
這歷史不可以使用let的,因為計算y的值的時候用到了新定義的x。
習題
【m】3.1 用lambda表達式寫一個等同于let星效果的表達式。可能需要多個lambda。
由于列表在Lisp中的重要地位,專門準備了特殊形式來操作列表頭部的元素添加和刪除,也就是說用列表實現(xiàn)棧。如果list是一個列表的位置的名字,那么(push x list)會更改list,將x加到list的第一個元素,(pop list)會返回第一個元素,并且有一個副作用,就是刪掉list的第一個元素。Push和pop等同于下面的表達式:
(push x list)
等同于
(setf list (cons x list))
(pop list)
等同于
(let ((result (first list)))
? (setf list (rest list))
?result)
列表可以對元素進行累加,一個和類似地可以對數(shù)字進行累加。Lisp提供incf和decf作為增加和減少一個數(shù)字的方法。第一個參數(shù)都必須是一個位置,之后的參數(shù)可以沒有,默認是1 。C語言中是由自增運算符++的,功能和這個類似。
(incf x)等于(incf x 1)等于(setf x (+ x 1))
(decf x)等于(decf x 1)等于(setf x (- x 1))
當位置指向的不是一個變量而是更加復雜的形式的話,Lisp會小心的將不對人格子形式求值的代碼擴展開。支持的有push,pop,incf和decf。接下來的例子中,我們有一組選手的列表,并要決定那一個選手的分數(shù)最高,贏得游戲。結構player由鍵值選手的分數(shù)和贏家的數(shù)目組成,函數(shù)determin-winner會在贏家的wins字段加上1.incf的擴展綁定了一個臨時的變量所以分類沒有完成兩次。
(defstruct player (score 0) (wins 0))
(defun determine-winner (players)
? “Increment the WINS for the player with highest score.”
? (incf (player-wins (first (sort players #’>
? ?:key #’player-score)))))
等同于
(defun determine-winner (players)
? “Increment the WINS for the player with highest score.”
? (let ((temp (first (sort players #’>:key #’player-score))))
? ? (setf (player-wins temp) (+ (player-wins temp) 1))))
用于迭代循環(huán)的函數(shù)和特殊形式
很多語言都有保留字用來支持迭代循環(huán)。例如,Pascal就有while,repeat和for語句。相對的Common Lisp有一個巨大的集合來選擇。見下表:
名字 | 功能 |
---|---|
Dolist | 循環(huán)列表中的元素 |
Dotimes | 循環(huán)連續(xù)的整數(shù) |
Do,do星 | 一般循環(huán),精簡語法 |
Loop | 一般循環(huán),冗長語法 |
Mapc,mapcar | 遍歷列表(多個列表)的元素 |
Some,every | 帶條件的遍歷列表 |
Find,reduce等等 | 更加具體的循環(huán)函數(shù) |
遞歸 | 一般迭代 |
為了解釋每一個選項,我們會介紹一個函數(shù)length,返回列表的元素個數(shù)。特殊形式dolist可以用來迭代列表的元素,語法是這樣:
(dolist (變量 列表 可選結果) 主體…)
對于列表中的每一個元素,主體部分都會被執(zhí)行一次,變量部分就是綁定在各個元素上,依次進行。最后dolist會求值并且返回可選結果表達式部分,如果沒有結果表達式的話就返回nil。
下面是使用dolist來定義的length版本。形式let引入了一個新的變量,len,初始化綁定為0。Dolist會對應每一個列表的元素執(zhí)行一次主體,并且給len加上1。這個實現(xiàn)的循環(huán)迭代變量element在主體中沒有使用,這一般不大會這樣。
(defun length1 (list)
? (let ((len 0)) ;一開始len等于0
? ? (dolist (element list) ;依次迭代
? ? ? (incf len)) ;len加1
? ?len)) ;返回len
Dolist中也能用上可選結果部分,很多程序猿都使用這種風格,我發(fā)現(xiàn)這樣有時很容易失去對結果的追蹤,所以還是建議將結果在最后顯式給出。
(defun length1.1 (list) ;可選結果的版本
? (let ((len 0)) ;這種不是特意這樣做的
? ? (dolist (element list len) ;這里使用len作為結果返回
? ? ? (incf len))))
函數(shù)mapc的功能和特殊形式dolist是一樣的。在最簡單的情況下,mapc只有兩個參數(shù),第一個是函數(shù),第二個是列表。將函數(shù)應用到列表的每一個元素上,下面是length的mapc版本:
(defun length2 (list)
?(let ((len 0)) ;開始len是0
? ? (mapc #’(lambda (element) ;迭代每一個元素
? ? ? ? (incf len)) ;len加上1
? ? ?list)
? ?len)) ;返回len
一共有七個像map什么一樣的定位函數(shù),其中最常用的就是mapc和mapcar。Mapcar和mapc功能一樣,只是最后的返回時一個結果的列表形式。
也有dotimes形式,語法是這樣:
(dotimes (變量 數(shù)字 可選結果) 主體…)
主體的執(zhí)行次數(shù)依據(jù)變量,變量一開始綁定到0,之后是1,一直到n-1,也就說是執(zhí)行了n次。當然,dotimes不適合用來實現(xiàn)length,因為我們不確定到底循環(huán)多少次。
兩個一般的循環(huán)格式,do和loop。Do的語法:
(do ((變量 初始值 遞進部分)…)
(脫離測試 結果)
主體…)
每一個變量都是綁定初始值來初始化。如果脫離測試部分是真,那么結果就會被反悔。另外,主體執(zhí)行一次,變量就會對應遞進部分設定一次,然后再次嘗試脫離測試。這個循環(huán)一直尺度到脫離測試部分為真。如果遞進部分被省略,對應的遞進部分就不會更新。這樣子的話就和let沒有區(qū)別了。
用do實現(xiàn)的length,引入了兩個變量,len用來存儲列表的元素個數(shù),l用來接收列表。L經(jīng)常會說是探底列表,因為他會一直對列表使用cdr操作。(實際上這里我們用更加容易記憶的rest來代替cdr。)請注意do循環(huán)是沒有主體的。所有的計算都在變量初始化和遞進階段完成,在測試部分結束。
(defun length3 (list)
? (do ((len 0 (+ len 1)) ;開始len等于0,遞增
? ? (l list (rest l))) ;一次次迭代
? ? ((null l)len))) ;直到列表的盡頭
Do是很難讓人明白,因為他沒有清晰的說明我們正在循環(huán)到哪里了。為了確認迭代列表的進度就需要查看l和測試部分。沒有變量來表示當前的元素在列表什么位置,我們必須用(first l)來代替查看。Dolist和mapc都設定了步進,測試和變量自動命名。就具體這個原則來說,都的確是不是很具體,所以本書中我們不太會使用do。但是很多優(yōu)秀的程序員喜歡使用do,所以即使我們不適用do,也要看明白do的運行。
Loop的語法有他自己的一整套語言,并且是和Common Lisp不類似的風格。我們不會列出loop的所有特性,僅僅給出一些例子,具體請參考CLTL第24章5小節(jié)。下面是一些使用loop實現(xiàn)的length版本:
(defun length4 (list)
?(loop for element in list;遍歷每一個元素
? ?count t));計數(shù)
(defun length5 (list)
? (loop for element in list; 遍歷每一個元素
? ?Summing 1));加一
(defun length6 (list)
(loop with len = 0;開始len是0
Until (null list);直到列表為空
For element = (pop list);依次迭代
Do (incf len);len加1
Finally (return len)));返回len
每一個程序員都有關于循環(huán)的一些經(jīng)驗,反復的實踐過。經(jīng)常就叫做編程慣用語法或者陳詞濫調。其中一個例子就是遍歷列表或者數(shù)組中的每一個元素然后做出相應的操作。在大部分語言中,這些慣用用法不會有一個顯式的語法標記,一般而言是用一個普通的循環(huán)結構實現(xiàn),然后讓讀者來理解分辨程序員在做什么。
Lisp的不同之處在于他提供的方式是顯式地封裝了這些慣用語法,并且用成型語法和特殊形式來指向他們。Dolist和dotimes就是兩個例子,都是遵循了“具體化”的原則。大部分程序員傾向于使用dolist而不是do。
除了特殊形式dolist和dotimes之外,還有一些函數(shù)是設計來處理一般的慣用語法的。比如count-if,就是計算序列中滿足斷言的元素的個數(shù),還有position-if,返回滿足斷言的元素的索引值。兩者都備用來實現(xiàn)length。在線面的length7,count-if給出了在list中滿足斷言的元素個數(shù)。函數(shù)斷言true的值總是真,元素都滿足,那么就會返回list的長度。
(defun length7 (list)
(count-if #’true list))
(defun true (x) t)
在length8中,函數(shù)position-if找到一個滿足斷言的元素的位置,位置計算是從列表的最后開始的。既然索引是從0開始,那么長度就需要加上1得到。必須承認,這不是一個最直接的length實現(xiàn)方法。
(defun length8 (list)
(if (null list)
0
(+ 1 (position-if #’true list :form-end t))))
一些函數(shù)的靈活設計可以讓函數(shù)處理急速所有的序列操作。這種靈活性主要是三種形式:第一,mapcar一類的函數(shù)可以操作任意數(shù)量的列表:
> (mapcar #'- '(1 2 3)) => (-1 -2 -3)
> (mapcar #'+ '(1 2) '(10 20)) => (11 22)
> (mapcar #'+ '(1 2) '(10 20) '(100 200)) => (111 222)
第二,很多函數(shù)是接受用戶定義的關鍵字,可以更改函數(shù)的比較測試功能,或者是只操作獲取序列的一部分。
(remove 1 '(1 23210 -1)) => (2 3 2 a -1)
(remove 1 '(1 23210 -1) :key #'abs) => (2 3 2 0)
(remove 1 '(123210 -1) :test #'<) => (1 1 a -1)
(remove 1 '(123210 -1) :start 4) => (1 2 3 2 a -1)
第三,一些對應的函數(shù)是以-if結尾或者-if-not結尾,就會接受一個斷言而不是一個元素作為匹配:
(remove-if #'oddp '(1 2 3 2 1 a -1)) => (2 2 0)
(remove-if-not #'oddp '(1 2 3 2 1 a -1)) => (1 3 1 -1)
(find-if #'evenp '(1 2 3 2 1 a -1)) => 2
下面的兩張表有著前提:
(setf x ‘(a b c))
(setf y ‘(1 2 3))
第一張表列出的函數(shù)是可以處理任意數(shù)量的列表,但是不接受關鍵字選項。
表達式 | 結果 | 說明 |
---|---|---|
(every #’oddp y) | nil | 測試是不是每一個元素都符合斷言 |
(some #’oddp y) | t | 測試一些元素是不是符合斷言 |
(mapcar #’- y) | (-1 -2 -3) | 將函數(shù)應用到每一個元素,然后返回結果 |
(mapc #’print y) | 打印1 2 3 | 對每一個元素執(zhí)行操作 |
第二張表列出的函數(shù)是由-if和-if-not的版本的函數(shù),也接受關鍵字函數(shù):
表達式 | 結果 | 說明 |
---|---|---|
(member 2 y) | (2 3) | 看看元素是不是在列表中 |
(count ‘b x) | 1 | 計算匹配的元素數(shù)量 |
(delete 1 y) | (2 3) | 輸出的時候省略匹配的元素 |
(find 2 y) | 2 | 找到匹配的第一個元素 |
(position ‘a(chǎn) x) | 0 | 在序列中找出元素的索引 |
(reduce #’+ y) | 6 | 將函數(shù)應用到后續(xù)的元素 |
(remove 2 y) | (1 3) | 和delete類似,但是會做一份新的拷貝 |
(substitute 4 2 y) | (1 4 3) | 用新元素替代舊元素 |
用遞歸來迭代
Lisp被稱作是一種遞歸的語言,意思就是Lisp鼓勵開發(fā)者使用函數(shù)的時候調用自身。正如上面縮進啊,在Common Lisp中有一些令人目眩的函數(shù)和特殊形式來寫循環(huán),但是也是,很多程序處理迭代的時候都不是用循環(huán)而是使用遞歸的。
Length的一個簡單定義就是空列表的長度為0,其他的列表都是自己的rest的長度加上1.這個定義就可以直接轉化成遞歸代碼。
(defun length9 (list)
? (if (null list)
? ?0
? ? (+ 1 (length9 (rest list)))))
這個版本的length定義和容易得出關于列表定義的遞歸型結論:一個列表要么事空列表,要么就是一個元素和另一個列表的組合。一般來說,大部分遞歸函數(shù)都是由他們要處理的數(shù)據(jù)的遞歸本質所衍生出來的。有些數(shù)據(jù),比如二進制樹,用遞歸函數(shù)以外的函數(shù)是很難處理的。另外像列表和整型,既可以用遞歸定義(緊接這就用遞歸函數(shù)),也可以序列定義(就會使用迭代函數(shù))。在本書中,我們傾向于用列表是一個序列的觀點,而不是元素和列表的組合的觀點。采取這種觀點的理由是列表作為一個first元素和rest列表的分割是一種認為的任意的分割,這種分割基于列表的實現(xiàn),而且實現(xiàn)的方式根據(jù)Lisp的不同而不同。但是還有很多其他的方式來分解一個列表。我們可以將列表看做最后一個元素和其他元素的組合,也可以看做是前半部分和后半部分的組合。列表作為一個序列的觀點就會去除人為定義的因素。將所有的元素平等對待。
由于低效,遞歸函數(shù)的使用也有一些反對的聲音,因為編譯器一定要給每一個遞歸調用分配內(nèi)存。對于length9,這的確是對的,但是并不是所有的遞歸函數(shù)調用都這樣。看看下面的函數(shù):
(defun length10 (list)
? (length10-aux list 0))
(defun length10-aux (sublist len-so-far)
? (if (null sublist)
? ?len-so-far
? ? (length10-aux (rest sublist) (+ 1 len-so-far))))
Length10使用length10-aux作為一個輔助函數(shù),將0作為列表的長度傳遞進去。Length10-aux之后會對list進行遍歷,對每一個元素加1。這里有個不變的關系就是子列表的長度加上len-so-far的值總是等于原始列表的長度的。因此,當子列表變?yōu)榭樟斜恚琹en-so-far就是原始列表的長度。像len-so-far這種變量,執(zhí)行追蹤結果的功能,一般被稱作收集器。其他使用收集器的函數(shù)例子包括之后的flatten-all函數(shù),one-unknown函數(shù)。還有之后討論的Prolog斷言,在后面的anonymous-variables-in中我們使用了兩個收集器。
Length9和length10之間的區(qū)別在于什么時候做加1。在length9中,函數(shù)調用本身,之后會返回,然后加1。在length10-aux中,函數(shù)是加1,然后調用自身,之后返回。在調用自身之后沒有任何其他的操作,所以編譯器可以將在遞歸之前原始調用分配的內(nèi)存全部釋放,length10-aux這種遞歸函數(shù)就叫做尾遞歸,因為遞歸調用出現(xiàn)在函數(shù)的最后,被稱作尾遞歸。很多遍一起都會優(yōu)化尾遞歸,但是所有的編譯器都會這么做。(第22章會更加詳細的介紹尾遞歸,而且在Scheme的便以其中,尾遞歸的編譯器優(yōu)化是一定會做的)。
有些人會覺得引入輔助函數(shù)length10-aux是一種很不優(yōu)雅的寫法,這里我們提供兩個其他的選擇。第一種,我們可以結合length10和length10-aux,寫到一個函數(shù)中,這需要使用一個可選參數(shù):
(defun length11 (list &optional (len-so-far 0))
? (if (null list)
? ?Len-so-far
? ? (length11 (rest list) (+ 1 len-so-far))))
第二種,我們可以在主函數(shù)本身的定義中引入一個本地函數(shù)。這可以用特殊形式labels實現(xiàn):
(defun length12 (the-list)
? (labels
? ? ((length13 (list len-so-far)
? ? ? (if (null list)
? ? ? ?Len-so-far
? ? ? ? (length13 (rest list) (+ 1 len-so-far)))))
? ? (length13 the-list 0)))
一般來說,labels形式(或者相似的flet形式)可以用來引入一個或多個本地函數(shù)。語法:
(labels
((函數(shù)名 (參數(shù)…) 函數(shù)體)…)
Labels的主體)
其他特殊形式
有一些特殊形式并不能夠進行嚴格的分類。我們看到了兩中創(chuàng)建常量和函數(shù)的特殊形式,quote和function。他們是有簡略形式的:單引號加上x代表(quote x),井號單引號f表示(function f)。
特殊形式progn可以用來對形式的序列求值,返回最后一個的值。
(prong (setf x 0) (setf x (+ x 1)) x) => 1
Prong在其他語言中相當于begin和end區(qū)塊,但是在Lisp中不是很常用。主要是兩個原因,第一,用函數(shù)式風格寫就的程序不需要序列操作,因為他們沒有副作用。第二,即使需要副作用,很多特殊形式的主體本身就是一個隱式的progn。Progn的使用我能想到的有三個地方。第一是條件分支的兩個選擇之一,可以在if中使用progn或者cond:
(if (> x 100)
? ?(progn (print “too big”)
? ?(setf x 100))
?x)
(cond ((> x 100)
? ?(print “too big”)
? ?(setf x 100))
?(t x))
如果條件式只有一個分支,那么就應該使用when或者unless,他們都隱含了progn。如果有超過兩個分支,就應該用cond。
第二,progn在宏之中有需要使用,用來擴展成多個頂層形式,就像在defun星中的例子。第三,有時候progn會用在unwind-protect,一種高級宏。之后會介紹一個例子,一個高級宏with-resource。
Trace和untrace用來控制函數(shù)調用和返回的調試信息。
> (trace length9) => (LENGTH9)
> (length9 '(a b c) =>
(1 ENTER LENGTH9: (A B C))
?(2 ENTER LENGTH9: (B C))
? ?(3 ENTER LENGTH9: (C)))
? ? ?(4 ENTER LENGTH9: NIL)
? ? ?(4 EXIT LENGTH9: 0)
? ?(3 EXIT LENGTH9: 1)
?(2 EXIT LENGTH9: 2)
(1 EXIT LENGTH9: 3)
3
?> (untrace length9) => (LENGTH9)
> (length9 '(a b c)) => 3
最后,特殊形式return可以用來從代碼塊中返回。塊使用特殊形式block來設置的,或者是用循環(huán)形式(do,do星,dolist,dotimes,或者loop)。例如,下面的函數(shù)計算的是列一個表中數(shù)字的乘積,但是如果數(shù)字中包含了0,那么乘積就肯定是0,所以就從dolist循環(huán)中返回0.請注意這里只是從dolist中返回,不是函數(shù)本身返回(雖然有可能dolist是函數(shù)的最后一個語句,返回值也就是函數(shù)的返回值)。一般要使用大寫的RETURN來標識這里是循環(huán)的出口。
(defun product (numbers)
?“Multiply all the numbers together to compute their product.”
? (let ((prod 1))
? ? (dolist (n numbers prod)
? ? ? (if ( = n 0)
? ? ? ? (RETURN 0)
? ? ? ? (setf prod (* n prod))))))
宏
接下來的討論是一些對于術語特殊形式的漫談。實際上這些特殊形式中的一些是用宏還是先的,編譯器會負責進行擴展。Common Lisp提供了內(nèi)建宏摒棄誒允許定義新的宏來擴展語言。(但是想定義新的特殊形式是不可能的)
宏是用特殊形式defmarco來定義的。結社我們想要定義一個宏,while,行為和Pascal中的while循環(huán)語句一樣。寫一個宏的過程分為4步:
考慮一下是不是真的需要這個宏
寫下宏的語法
指明宏應該擴展成什么樣子
使用defmarco來實現(xiàn)對應的語法
第一步就是考慮需不需要一個新的宏,因為這就是和定義一門新的語言一樣。這種看法使得開發(fā)者在定義新的宏的時候極端謹慎。(另外,當有人問起,“你今天干了些什么?”,說,“定意義了一門新的語言嗎,并寫了一個編譯器”肯定要比回答“寫了一些宏”要拉風很多)。引入一個宏要比引入一個函數(shù),給你的程序的閱讀者增加的負擔大得多,所以不要輕易定義宏。只有當非常確定需要宏的時候,考慮引入宏會和現(xiàn)有的系統(tǒng)完美配合。就如C.A.R.Hoare說過,“語言設計不應該做得事情就是引入自己沒有考慮過的概念。”
第二步就是考慮宏應該展開成什么。無論如何,遵循已有的Lisp約定俗成的宏語法是一個好的習慣。看一下用于循環(huán)的宏(dolist,dotimes,do-symbols),用于定義的宏(defun,defvar,defperameter,defstruct),或者是I/O宏(wit-open-file,with-open-system,with-input-from-string)。如果你遵循命名和語法的規(guī)則,而不是自行其是的話,對于你的代碼的閱讀者會省心很多,你自己看代碼也會省心很多。對于while,一個好的語法就是:
(while test body…)
第三步就是寫下宏調用的展開代碼:
(loop
?(unless test (return nil))
body)
最后一步就是使用defmarco寫下宏的定義。defmarco形式很接近于defun,也有參數(shù)列表,可選文檔字符串和主體。也有一些區(qū)別,在參數(shù)列表的處理上有區(qū)別,之后我們會解釋。下面是一個宏while的定義,有一個test部分和一個body部分,用的是之前l(fā)oop代碼:
(defmarco while (test &rest body)
?“Repeat body while test is true.”
?(list* ‘loop
? ?(list ‘unless test ‘(return nil))
? ?body))
(這個list星函數(shù)和list函數(shù)很像,除了一點,他會將最后一個參數(shù)追加到其他參數(shù)的列表的末尾)通過使用macroexpand,我們可以看到宏的展開形式:
> (macroexpand-1 '(while < i 10)
(print (* i i))
(setf i (+ i 1)))) =>
(LOOP (UNLESS < I 10) (RETURN NIL))
(PRINT (* I I))
(SETF I (+ I I)))
?> (setf i 7) => 7
> (while (< i 10)
(print (* i i))
(setf i (+ i 1))) =>
?49
64
81
NIL
之后我們會描述一個更加復雜的宏,和寫宏的時候容易出現(xiàn)的陷阱和錯誤。
反引號標記
定義宏while最難的部分就是構建宏擴展的代碼。如果有更加方便的方法也是不錯的,下面的while版本就是這么個嘗試。他定義一個本地變量code作為我們最終代碼的模板,之后用實際的代碼來代替代碼中的test和body等占位符號。這個用函數(shù)subst實現(xiàn)(subst new old tree),這個語句會把每一個old在tree中出現(xiàn)的地方替代成new。
(defmarco while (test &rest body)
? “Repeat body while test is true.”
? (let ((code ‘(loop (unless test (return nil)) . body)))
??(subst test ‘test (subst body ‘body code))))
用組件構建代碼或者非代碼數(shù)據(jù),經(jīng)常需要一種特殊的標記,就是反引號標記。反引號字符“”和引號字符“’”很像。一個反引號表示接下來基本上是一個字面意義上的或許包含了一些需要求值的部分的表達式,任何標記有前導逗號“,”的符號都會被求值,之后插入到結構中,任何有前導符號“,@”的符號必須求值一個由分隔符分割結構的列表:被插入到每一個元素之間,沒有頂層括號。標記的細節(jié)我們在第23.5小節(jié)介紹。下面是使用了反引號和逗號重寫的while:
(defmarco while (test &rest body)“Repeat body while test is true.”
``(loop (unless ,test (return nil))
? ?,@body))
下面是一些反引號的例子。請注意列表的結尾,“,@”的效果和“.”之后跟上一個逗號“,”的效果是一樣的。在列表中間,只可以使用“,@”。
> (setf testl '(a test)) => (A TEST)
?>
(this is ,testl) => (THIS IS (A TEST)) >
(this is ,@testl) => (THIS IS A TEST)
>
(this is ,testl) => (THIS IS A TEST) ?>
(this is ,@testl -- this is only ,@testl) => (THIS IS A TEST -- THIS IS ONLY A TEST)
這一小節(jié)就完整介紹了特殊形式和宏。本章的其余小節(jié)就介紹Common Lisp中重要的內(nèi)建函數(shù)。
3.3 操作列表的函數(shù)
為了舉例子方便,我們先做如下假設:
(setf x ‘(a b c))
(setf y ‘(1 2 3))
我們在這里總結一下列表操作中最重要的函數(shù),更加復雜的一些函數(shù)我們用到的時候再解釋。
函數(shù)的表達式 | 求值結果 | 功能 |
---|---|---|
(first x) | =>a | 列表的第一個元素 |
(second x) | =>b | 列表的第二個元素 |
(third x) | =>c | 列表的第三個元素 |
(nth 0 x) | =>a | 從0開始數(shù),列表的第n個元素 |
(rest x) | =>(b c) | 除了第一個元素之外的所有元素 |
(car x) | =>a | first的別名 |
(cdr x) | =>(b c) | rest的別名 |
(last x) | =>(c) | 列表中的最后一個組合單元 |
(length x) | =>3 | 列表中元素的數(shù)目 |
(reverse x) | =>(c b a) | 元素逆序的列表 |
(cons 0 y) | =>(0 1 2 3) | 在列表的最前面加上元素 |
(append x y) | =>(a b c 1 2 3) | 聚合列表的元素 |
(list x y) | =>((a b c) (1 2 3)) | 創(chuàng)建一個新列表 |
(list* 1 2 x) | =>(1 2 a b c) | 將最后一個參數(shù)追加到其他元素后面 |
(null nil) | =>T | 是否為空斷言為真 |
(null x) | =>nil | 是否為空斷言為假 |
(listp x) | =>T | 是否為列表斷言為真 |
(listp 3) | =>nil | 是否為列表斷言為假 |
(consp x) | =>t | 是否為列表斷言為真 |
(consp nil) | =>nil | 是否為原子斷言為假 |
(equal x x) | =>t | 列表相等為真 |
(equal x y) | =>nil | 列表不等為假 |
(sort y #’>) | =>(3 2 1) | 根據(jù)比較函數(shù)對列表排序 |
(subseq x 1 2) | =>(B) | 根據(jù)給出的起始點和結束點截取子序列 |
表達式(cons a b)會將元素a加到列表b的前面,形成一個更長的列表,但要是b不是一個列表呢?其實也不會出錯的,它的結果我們假設是一個叫做x的對象,對象的性質(first x)結果是a,(rest x)的結果是b,x的打印形式就是(a . b)。這個標記被稱作點對標記。如果b是一個列表,那么普通的列表打印就可以用了,但是兩種標記都不能同時用來輸入。
現(xiàn)在為止,我們使用列表就是序列的觀點,用的是“三個元素的列表”這樣的意思。列表是一個很方便的抽象模型,但是實際上底層的列表的實現(xiàn)是構筑在叫做組合單元的基礎上。一個組合單元就是一種具有兩個域的數(shù)據(jù)結構:頭和尾。一個三元素的列表,也可以看做一個組合單元,頭指向的是第一個元素,尾指向的下一個單元,第二個單元的頭指向第二個元素,尾指向第三個單元,第三哥哥單元的頭指向第三個元素,尾是nil。所有普通的列表的尾都是nil。下圖表示的就是這種數(shù)據(jù)結構。
此圖表示的是三元素列表的內(nèi)部結構,還有(cons ‘one ‘two)的結果結構。
習題
【s】函數(shù)cons可以看做是之前列出的函數(shù)的一個特殊情況,哪一個?
【m】寫一個打印點對標記的函數(shù)。使用內(nèi)建函數(shù)princ來的打印表達式的每一個組件。
【m】寫一個函數(shù),類似于常規(guī)的print函數(shù),需要時打印常規(guī)的點對表達式,但是也可以打印列表表示形式。