本章是選擇性閱讀的。本章描述了 Common Lisp 里一些更深奧的特性。Common Lisp 像是一個冰山:大部分的功能對于那些永遠不需要他們的多數(shù)用戶是看不見的。你或許永遠不需要自己定義包 (Package)或讀取宏 (read-macros),但當(dāng)你需要時,有些例子可以讓你參考是很有用的。
類型在 Common Lisp 里不是對象。舉例來說,沒有對象對應(yīng)到?integer
?這個類型。我們像是從?type-of
?函數(shù)里所獲得的,以及作為傳給像是?typep
?函數(shù)的參數(shù),不是一個類型,而是一個類型標識符 (type specifier)。
一個類型標識符是一個類型的名稱。最簡單的類型標識符是像是?integer
?的符號。這些符號形成了 Common Lisp 里的類型層級。在層級的最頂端是類型?t
?── 所有的對象皆為類型?t
?。而類型層級不是一棵樹。從?nil
?至頂端有兩條路,舉例來說:一條從?atom
,另一條從?list
?與?sequence
?。
一個類型實際上只是一個對象集合。這意味著有多少類型就有多少個對象的集合:一個無窮大的數(shù)目。我們可以用原子的類型標識符 (atomic type specifiers)來表示某些集合:比如?integer
?表示所有整數(shù)集合。但我們也可以建構(gòu)一個復(fù)合類型標識符 (compound type specifiers)來參照到任何對象的集合。
舉例來說,如果?a
?與?b
?是兩個類型標識符,則?(or?a?b)
?表示分別由?a
?與?b
?類型所表示的聯(lián)集 (union)。也就是說,一個類型(or?a?b)
?的對象是類型?a
?或 類型?b
?。
如果?circular?
?是一個對于?cdr
?為環(huán)狀的列表返回真的函數(shù),則你可以使用適當(dāng)?shù)男蛄屑蟻肀硎荆?[1]
(or vector (and list (not (satisfies circular?))))
某些原子的類型標識符也可以出現(xiàn)在復(fù)合類型標識符。要表示介于 1 至 100 的整數(shù)(包含),我們可以用:
(integer 1 100)
這樣的類型標識符用來表示一個有限的類型 (finite type)。
在一個復(fù)合類型標識符里,你可以通過在一個參數(shù)的位置使用?*
?來留下某些未指定的信息。所以
(simple-array fixnum (* *))
描述了指定給?fixnum
?使用的二維簡單數(shù)組 (simple array)集合,而
(simple-array fixnum *)
描述了指定給?finxnum
?使用的簡單數(shù)組集合 (前者的超類型 「supertype」)。尾隨的星號可以省略,所以上個例子可以寫為:
(simple-array fixnum)
若一個復(fù)合類型標識符沒有傳入?yún)?shù),你可以使用一個原子。所以?simple-array
?描述了所有簡單數(shù)組的集合。
如果有某些復(fù)合類型標識符你想重復(fù)使用,你可以使用?deftype
?定義一個縮寫。這個宏與?defmacro
?相似,但會展開成一個類型標識符,而不是一個表達式。通過表達
(deftype proseq ()
'(or vector (and list (not (satisfies circular?)))))
我們定義了?proseq
?作為一個新的原子類型標識符:
> (typep #(1 2) 'proseq)
T
如果你定義一個接受參數(shù)的類型標識符,參數(shù)會被視為 Lisp 形式(即沒有被求值),與?defmacro
?一樣。所以
(deftype multiple-of (n)
`(and integer (satisfies (lambda (x)
(zerop (mod x ,n))))))
(譯注: 注意上面代碼是使用反引號?`````?)
定義了?(multiple-of n)?當(dāng)成所有?n
?的倍數(shù)的標識符:
> (type 12 '(multiple-of 4))
T
類型標識符會被直譯 (interpreted),因此很慢,所以通常你最好定義一個函數(shù)來處理這類的測試。
第 7 章曾提及的流有二進制流 (binary streams)以及字符流 (character streams)。一個二進制流是一個整數(shù)的來源及/或終點,而不是字符。你通過指定一個整數(shù)的子類型來創(chuàng)建一個二進制流 ── 當(dāng)你打開流時,通常是用?unsigned-byte
?── 來作為?:element-type
?的參數(shù)。
關(guān)于二進制流的 I/O 函數(shù)僅有兩個,?read-byte
?以及?write-byte
?。所以下面是如何定義復(fù)制一個文件的函數(shù):
(defun copy-file (from to)
(with-open-file (in from :direction :input
:element-type 'unsigned-byte)
(with-open-file (out to :direction :output
:element-type 'unsigned-byte)
(do ((i (read-byte in nil -1)
(read-byte in nil -1)))
((minusp i))
(declare (fixnum i))
(write-byte i out)))))
僅通過指定?unsigned-byte
?給?:element-type
?,你讓操作系統(tǒng)選擇一個字節(jié) (byte)的長度。舉例來說,如果你明確地想要讀寫 7 比特的整數(shù),你可以使用:
(unsigned-byte 7)
來傳給?:element-type
?。
7.5 節(jié)介紹過宏字符 (macro character)的概念,一個對于?read
?有特別意義的字符。每一個這樣的字符,都有一個相關(guān)聯(lián)的函數(shù),這函數(shù)告訴?read
?當(dāng)遇到這個字符時該怎么處理。你可以變更某個已存在宏字符所相關(guān)聯(lián)的函數(shù),或是自己定義新的宏字符。
函數(shù)?set-macro-character
?提供了一種方式來定義讀取宏 (read-macros)。它接受一個字符及一個函數(shù),因此當(dāng)?read
?碰到該字符時,它返回調(diào)用傳入函數(shù)后的結(jié)果。
Lisp 中最古老的讀取宏之一是?'
?,即?quote
?。我們可以定義成:
(set-macro-character #\'
#'(lambda (stream char)
(list (quote quote) (read stream t nil t))))
當(dāng)?read
?在一個普通的語境下遇到?'
?時,它會返回在當(dāng)前流和字符上調(diào)用這個函數(shù)的結(jié)果。(這個函數(shù)忽略了第二個參數(shù),第二個參數(shù)永遠是引用字符。)所以當(dāng)?read
?看到?'a
?時,會返回?(quote?a)
?。
譯注:?read
?函數(shù)接受的參數(shù)?(read?&optional?stream?eof-error?eof-value?recursive)
現(xiàn)在我們明白了?read
?最后一個參數(shù)的用途。它表示無論?read
?調(diào)用是否在另一個?read
?里。傳給?read
?的參數(shù)在幾乎所有的讀取宏里皆相同:傳入?yún)?shù)有流 (stream);接著是第二個參數(shù),?t
?,說明了?read
?若讀入的東西是 end-of-file 時,應(yīng)不應(yīng)該報錯;第三個參數(shù)說明了不報錯時要返回什么,因此在這里也就不重要了;而第四個參數(shù)?t
?說明了這個?read
?調(diào)用是遞歸的。
(譯注:困惑的話可以看看?read 的定義?)
你可以(通過使用?make-dispatch-macro-character
?)來定義你自己的派發(fā)宏字符(dispatching macro character),但由于?#
已經(jīng)是一個宏字符,所以你也可以直接使用。六個?#
?打頭的組合特別保留給你使用:?#!
?、?#?
?、?##[
?、?##]
?、?#{
?、?#}
?。
你可以通過調(diào)用?set-dispatch-macro-character
?定義新的派發(fā)宏字符組合,與?set-macro-character
?類似,除了它接受兩個字符參數(shù)外。下面的代碼定義了?#?
?作為返回一個整數(shù)列表的讀取宏。
(set-dispatch-macro-character #\# #\?
#'(lambda (stream char1 char2)
(list 'quote
(let ((lst nil))
(dotimes (i (+ (read stream t nil t) 1))
(push i lst))
(nreverse lst)))))
現(xiàn)在?#?n
?會被讀取成一個含有整數(shù)?0
?至?n
?的列表。舉例來說:
> #?7
(1 2 3 4 5 6 7)
除了簡單的宏字符,最常定義的宏字符是列表分隔符 (list delimiters)。另一個保留給用戶的字符組是?#{
?。以下我們定義了一種更復(fù)雜的左括號:
(set-macro-character #\} (get-macro-character #\)))
(set-dispatch-macro-character #\# #\{
#'(lambda (stream char1 char2)
(let ((accum nil)
(pair (read-delimited-list #\} stream t)))
(do ((i (car pair) (+ i 1)))
((> i (cadr pair))
(list 'quote (nreverse accum)))
(push i accum)))))
這定義了一個這樣形式?#{x?y}
?的表達式,使得這樣的表達式被讀取為所有介于?x
?與?y
?之間的整數(shù)列表,包含?x
?與?y
?:
> #{2 7}
(2 3 4 4 5 6 7)
函數(shù)?read-delimited-list
?正是為了這樣的讀取宏而生的。它的第一個參數(shù)是被視為列表結(jié)束的字符。為了使?}
?被識別為分隔符,必須先給它這個角色,所以程序在開始的地方調(diào)用了?set-macro-character
?。
如果你想要在定義一個讀取宏的文件里使用該讀取宏,則讀取宏的定義應(yīng)要包在一個?eval-when
?表達式里,來確保它在編譯期會被求值。不然它的定義會被編譯,但不會被求值,直到編譯文件被載入時才會被求值。
一個包是一個將名字映對到符號的 Lisp 對象。當(dāng)前的包總是存在全局變量?*package*
?里。當(dāng) Common Lisp 啟動時,當(dāng)前的包會是*common-lisp-user*
?,通常稱為用戶包 (user package)。函數(shù)?package-name
?返回包的名字,而?find-package
?返回一個給定名稱的包:
> (package-name *package*)
"COMMON-LISP-USER"
> (find-package "COMMON-LISP-USER")
#<Package "COMMON-LISP-USER" 4CD15E>
通常一個符號在讀入時就被 interned 至當(dāng)前的包里面了。函數(shù)?symbol-package
?接受一個符號并返回該符號被 interned 的包。
(symbol-package 'sym)
#<Package "COMMON-LISP-USER" 4CD15E>
有趣的是,這個表達式返回它該返回的值,因為表達式在可以被求值前必須先被讀入,而讀取這個表達式導(dǎo)致?sym
?被 interned。為了之后的用途,讓我們給?sym
?一個值:
> (setf sym 99)
99
現(xiàn)在我們可以創(chuàng)建及切換至一個新的包:
> (setf *package* (make-package 'mine
:use '(common-lisp)))
#<Package "MINE" 63390E>
現(xiàn)在應(yīng)該會聽到詭異的背景音樂,因為我們來到一個不一樣的世界了: 在這里?sym
?不再是本來的?sym
?了。
MINE> sym
Error: SYM has no value
為什么會這樣?因為上面我們設(shè)為 99 的?sym
?與?mine
?里的?sym
?是兩個不同的符號。?[2]?要在用戶包之外參照到原來的?sym
?,我們必須把包的名字加上兩個冒號作為前綴:
MINE> common-lisp-user::sym
99
所以有著相同打印名稱的不同符號能夠在不同的包內(nèi)共存??梢杂幸粋€?sym
?在?common-lisp-user
?包,而另一個?sym
?在?mine
?包,而他們會是不一樣的符號。這就是包存在的意義。如果你在分開的包內(nèi)寫你的程序,你大可放心選擇函數(shù)與變量的名字,而不用擔(dān)心某人使用了同樣的名字。即便是他們使用了同樣的名字,也不會是相同的符號。
包也提供了信息隱藏的手段。程序應(yīng)通過函數(shù)與變量的名字來參照它們。如果你不讓一個名字在你的包之外可見的話,那么另一個包中的代碼就無法使用或者修改這個名字所參照的對象。
通常使用兩個冒號作為包的前綴也是很差的風(fēng)格。這么做你就違反了包本應(yīng)提供的模塊性。如果你不得不使用一個雙冒號來參照到一個符號,這是因為某人根本不想讓你用。
通常我們應(yīng)該只參照被輸出 (?exported?)的符號。如果我們回到用戶包里,并輸出一個被 interned 的符號,
MINE> (in-package common-lisp-user)
#<Package "COMMON-LISP-USER" 4CD15E>
> (export 'bar)
T
> (setf bar 5)
5
我們使這個符號對于其它的包是可視的。現(xiàn)在當(dāng)我們回到?mine
?,我們可以僅使用單冒號來參照到?bar
?,因為他是一個公開可用的名字:
> (in-package mine)
#<Package "MINE" 63390E>
MINE> common-lisp-user:bar
5
通過把?bar
?輸入 (?import
?)至?mine
?包,我們就能進一步讓?mine
?和?user
?包可以共享?bar
?這個符號:
MINE> (import 'common-lisp-user:bar)
T
MINE> bar
5
在輸入?bar
?之后,我們根本不需要用任何包的限定符 (package qualifier),就能參照它了。這兩個包現(xiàn)在共享了同樣的符號;不可能會有一個獨立的?mine:bar
?了。
要是已經(jīng)有一個了怎么辦?在這種情況下,?import
?調(diào)用會產(chǎn)生一個錯誤,如下面我們試著輸入?sym
?時便知:
MINE> (import 'common-lisp-user::sym)
Error: SYM is already present in MINE.
在此之前,當(dāng)我們試著在?mine
?包里對?sym
?進行了一次不成功的求值,我們使?sym
?被 interned 至?mine
?包里。而因為它沒有值,所以產(chǎn)生了一個錯誤,但輸入符號名的后果就是使這個符號被 intern 進這個包。所以現(xiàn)在當(dāng)我們試著輸入?sym
?至?mine
?包里,已經(jīng)有一個相同名稱的符號了。
另一個方法來獲得別的包內(nèi)符號的存取權(quán)是使用(?use
?)它:
MINE> (use-package 'common-lisp-user)
T
現(xiàn)在所有由用戶包 (譯注: common-lisp-user 包)所輸出的符號,可以不需要使用任何限定符在?mine
?包里使用。(如果?sym
?已經(jīng)被用戶包輸出了,這個調(diào)用也會產(chǎn)生一個錯誤。)
含有自帶操作符及變量名字的包叫做?common-lisp
?。由于我們將這個包的名字在創(chuàng)建?mine
?包時作為?make-package
?的?:use
?參數(shù),所有的 Common Lisp 自帶的名字在?mine
?里都是可視的:
MINE> #'cons
#<Compiled-Function CONS 462A3E>
在編譯后的代碼中, 通常不會像這樣在頂層進行包的操作。更常見的是包的調(diào)用會包含在源文件里。通常,只要把?in-package
?和defpackage
?放在源文件的開頭就可以了,正如 137 頁所示。
這種由包所提供的模塊性實際上有點奇怪。我們不是對象的模塊 (modules),而是名字的模塊。
每一個使用了?common-lisp
?的包,都可以存取?cons
?,因為?common-lisp
?包里有一個叫這個名字的函數(shù)。但這會導(dǎo)致一個名字為cons
?的變量也會在每個使用了?common-lisp
?包里是可視的。如果包使你困惑,這就是主要的原因;因為包不是基于對象而是基于名字。
loop
?宏最初是設(shè)計來幫助無經(jīng)驗的 Lisp 用戶來寫出迭代的代碼。與其撰寫 Lisp 代碼,你用一種更接近英語的形式來表達你的程序,然后這個形式被翻譯成 Lisp。不幸的是,?loop
?比原先設(shè)計者預(yù)期的更接近英語:你可以在簡單的情況下使用它,而不需了解它是如何工作的,但想在抽象層面上理解它幾乎是不可能的。
如果你是曾經(jīng)計劃某天要理解?loop
?怎么工作的許多 Lisp 程序員之一,有一些好消息與壞消息。好消息是你并不孤單:幾乎沒有人理解它。壞消息是你永遠不會理解它,因為 ANSI 標準實際上并沒有給出它行為的正式規(guī)范。
這個宏唯一的實際定義是它的實現(xiàn)方式,而唯一可以理解它(如果有人可以理解的話)的方法是通過實例。ANSI 標準討論?loop
?的章節(jié)大部分由例子組成,而我們將會使用同樣的方式來介紹相關(guān)的基礎(chǔ)概念。
第一個關(guān)于?loop
?宏我們要注意到的是語法 (?syntax?)。一個?loop
?表達式不是包含子表達式而是子句 (clauses)。這些子句不是由括號分隔出來;而是每種都有一個不同的語法。在這個方面上,?loop
?與傳統(tǒng)的 Algol-like 語言相似。但其它?loop
?獨特的特性,使得它與 Algol 不同,也就是在?loop
?宏里調(diào)換子句的順序與會發(fā)生的事情沒有太大的關(guān)聯(lián)。
一個?loop
?表達式的求值分為三個階段,而一個給定的子句可以替多于一個的階段貢獻代碼。這些階段如下:
loop
?表達式的返回值(可能返回多個值)。我們會看幾個?loop
?子句的例子,并考慮何種代碼會貢獻至何個階段。
舉例來說,最簡單的?loop
?表達式,我們可能會看到像是下列的代碼:
> (loop for x from 0 to 9
do (princ x))
0123456789
NIL
這個?loop
?表達式印出從?0
?至?9
?的整數(shù),并返回?nil
?。第一個子句,
for?x?from?0?to?9
貢獻代碼至前兩個階段,導(dǎo)致?x
?在序幕中被設(shè)為?0
?,在主體開頭與?9
?來做比較,在主體結(jié)尾被遞增。第二個子句,
do?(princ?x)
貢獻代碼給主體。
一個更通用的?for
?子句說明了起始與更新的形式 (initial and update form)。停止迭代可以被像是?while
?或?until
?子句來控制。
> (loop for x = 8 then (/ x 2)
until (< x 1)
do (princ x))
8421
NIL
你可以使用?and
?來創(chuàng)建復(fù)合的?for
?子句,同時初始及更新兩個變量:
> (loop for x from 1 to 4
and y from 1 to 4
do (princ (list x y)))
(1 1)(2 2)(3 3)(4 4)
NIL
要不然有多重?for
?子句時,變量會被循序更新。
另一件在迭代代碼通常會做的事是累積某種值。舉例來說:
> (loop for x in '(1 2 3 4)
collect (1+ x))
(2 3 4 5)
在?for
?子句使用?in
?而不是?from
?,導(dǎo)致變量被設(shè)為一個列表的后續(xù)元素,而不是連續(xù)的整數(shù)。
在這個情況里,?collect
?子句貢獻代碼至三個階段。在序幕,一個匿名累加器 (anonymous accumulator)設(shè)為?nil
?;在主體裡,(1+?x)
?被累加至這個累加器,而在閉幕時返回累加器的值。
這是返回一個特定值的第一個例子。有用來明確指定返回值的子句,但沒有這些子句時,一個?collect
?子句決定了返回值。所以我們在這里所做的其實是重復(fù)了?mapcar
?。
loop
?最常見的用途大概是蒐集調(diào)用一個函數(shù)數(shù)次的結(jié)果:
> (loop for x from 1 to 5
collect (random 10))
(3 8 6 5 0)
這里我們獲得了一個含五個隨機數(shù)的列表。這跟我們定義過的?map-int
?情況類似 (105 頁「譯注: 6.4 小節(jié)?!?。如果我們有了?loop
,為什么還需要?map-int
??另一個人也可以說,如果我們有了?map-int
?,為什么還需要?loop
??
一個?collect
?子句也可以累積值到一個有名字的變量上。下面的函數(shù)接受一個數(shù)字的列表并返回偶數(shù)與奇數(shù)列表:
(defun even/odd (ns)
(loop for n in ns
if (evenp n)
collect n into evens
else collect n into odds
finally (return (values evens odds))))
一個?finally
?子句貢獻代碼至閉幕。在這個情況它指定了返回值。
一個?sum
?子句和一個?collect
?子句類似,但?sum
?子句累積一個數(shù)字,而不是一個列表。要獲得?1
?至?n
?的和,我們可以寫:
(defun sum (n)
(loop for x from 1 to n
sum x))
loop
?更進一步的細節(jié)在附錄 D 討論,從 325 頁開始。舉個例子,圖 14.1 包含了先前章節(jié)的兩個迭代函數(shù),而圖 14.2 演示了將同樣的函數(shù)翻譯成?loop
?。
(defun most (fn lst)
(if (null lst)
(values nil nil)
(let* ((wins (car lst))
(max (funcall fn wins)))
(dolist (obj (cdr lst))
(let ((score (funcall fn obj)))
(when (> score max)
(setf wins obj
max score))))
(values wins max))))
(defun num-year (n)
(if (< n 0)
(do* ((y (- yzero 1) (- y 1))
(d (- (year-days y)) (- d (year-days y))))
((<= d n) (values y (- n d))))
(do* ((y yzero (+ y 1))
(prev 0 d)
(d (year-days y) (+ d (year-days y))))
((> d n) (values y (- n prev))))))
圖 14.1 不使用 loop 的迭代函數(shù)
(defun most (fn lst)
(if (null lst)
(values nil nil)
(loop with wins = (car lst)
with max = (funcall fn wins)
for obj in (cdr lst)
for score = (funcall fn obj)
when (> score max)
(do (setf wins obj
max score)
finally (return (values wins max))))))
(defun num-year (n)
(if (< n 0)
(loop for y downfrom (- yzero 1)
until (<= d n)
sum (- (year-days y)) into d
finally (return (values (+ y 1) (- n d))))
(loop with prev = 0
for y from yzero
until (> d n)
do (setf prev d)
sum (year-days y) into d
finally (return (values (- y 1)
(- n prev))))))
圖 14.2 使用 loop 的迭代函數(shù)
一個?loop
?的子句可以參照到由另一個子句所設(shè)置的變量。舉例來說,在?even/odd
?的定義里面,?finally
?子句參照到由兩個collect
?子句所創(chuàng)建的變量。這些變量之間的關(guān)系,是?loop
?定義最含糊不清的地方??紤]下列兩個表達式:
(loop for y = 0 then z
for x from 1 to 5
sum 1 into z
finally (return y z))
(loop for x from 1 to 5
for y = 0 then z
sum 1 into z
finally (return y z))
它們看起來夠簡單 ── 每一個有四個子句。但它們返回同樣的值嗎?它們返回的值多少?你若試著在標準中想找答案將徒勞無功。每一個?loop
?子句本身是夠簡單的。但它們組合起來的方式是極為復(fù)雜的 ── 而最終,甚至標準里也沒有明確定義。
由于這類原因,使用?loop
?是不推薦的。推薦?loop
?的理由,你最多可以說,在像是圖 14.2 這般經(jīng)典的例子中,?loop
?讓代碼看起來更容易理解。
在 Common Lisp 里,狀況 (condition)包括了錯誤以及其它可能在執(zhí)行期發(fā)生的情況。當(dāng)一個狀況被捕捉時 (signalled),相應(yīng)的處理程序 (handler)會被調(diào)用。處理錯誤狀況的缺省處理程序通常會調(diào)用一個中斷循環(huán) (break-loop)。但 Common Lisp 提供了多樣的操作符來捕捉及處理錯誤。要覆寫缺省的處理程序,甚至是自己寫一個新的處理程序也是有可能的。
多數(shù)的程序員不會直接處理狀況。然而有許多更抽象的操作符使用了狀況,而要了解這些操作符,知道背后的原理是很有用的。
Common lisp 有數(shù)個操作符用來捕捉錯誤。最基本的是?error
?。一個調(diào)用它的方法是給入你會給?format
?的相同參數(shù):
> (error "Your report uses ~A as a verb." 'status)
Error: Your report uses STATUS as a verb
Options: :abort, :backtrace
>>
如上所示,除非這樣的狀況被處理好了,不然執(zhí)行就會被打斷。
用來捕捉錯誤的更抽象操作符包括了?ecase
?、?check-type
?以及?assert
?。前者與?case
?相似,要是沒有鍵值匹配時會捕捉一個錯誤:
> (ecase 1 (2 3) (4 5))
Error: No applicable clause
Options: :abort, :backtrace
>>
普通的?case
?在沒有鍵值匹配時會返回?nil
?,但由于利用這個返回值是很差的編碼風(fēng)格,你或許會在當(dāng)你沒有?otherwise
?子句時使用?ecase
?。
check-type
?宏接受一個位置,一個類型名以及一個選擇性字符串,并在該位置的值不是預(yù)期的類型時,捕捉一個可修正的錯誤 (correctable error)。一個可修正錯誤的處理程序會給我們一個機會來提供一個新的值:
> (let ((x '(a b c)))
(check-type (car x) integer "an integer")
x)
Error: The value of (CAR X), A, should be an integer.
Options: :abort, :backtrace, :continue
>> :continue
New value of (CAR X)? 99
(99 B C)
>
在這個例子里,?(car?x)
?被設(shè)為我們提供的新值,并重新執(zhí)行,返回了要是?(car?x)
?本來就包含我們所提供的值所會返回的結(jié)果。
這個宏是用更通用的?assert
?所定義的,?assert
?接受一個測試表達式以及一個有著一個或多個位置的列表,伴隨著你可能傳給error
?的參數(shù):
> (let ((sandwich '(ham on rye)))
(assert (eql (car sandwich) 'chicken)
((car sandwich))
"I wanted a ~A sandwich." 'chicken)
sandwich)
Error: I wanted a CHICKEN sandwich.
Options: :abort, :backtrace, :continue
>> :continue
New value of (CAR SANDWICH)? 'chicken
(CHICKEN ON RYE)
要建立新的處理程序也是可能的,但大多數(shù)程序員只會間接的利用這個可能性,通過使用像是?ignore-errors
?的宏。如果它的參數(shù)沒產(chǎn)生錯誤時像在?progn
?里求值一樣,但要是在求值過程中,不管什么參數(shù)報錯,執(zhí)行是不會被打斷的。取而代之的是,?ignore-errors
?表達式會直接返回兩個值:?nil
?以及捕捉到的狀況。
舉例來說,如果在某個時候,你想要用戶能夠輸入一個表達式,但你不想要在輸入是語法上不合時中斷執(zhí)行,你可以這樣寫:
(defun user-input (prompt)
(format t prompt)
(let ((str (read-line)))
(or (ignore-errors (read-from-string str))
nil)))
若輸入包含語法錯誤時,這個函數(shù)僅返回?nil
?:
> (user-input "Please type an expression")
Please type an expression> #%@#+!!
NIL
腳注
[1] | 雖然標準沒有提到這件事,你可以假定?and
?以及?or
?類型標示符僅考慮它們所要考慮的參數(shù),與?or
?及?and
?宏類似。
[2] | 某些 Common Lisp 實現(xiàn),當(dāng)我們不在用戶包下時,會在頂層提示符前打印包的名字。
更多建議: