Lisp 代碼是由 Lisp 對(duì)象的列表來表示。2.3 節(jié)宣稱這讓 Lisp 可以寫出可自己寫程序的程序。本章將示范如何跨越表達(dá)式與代碼的界線。
如何產(chǎn)生表達(dá)式是很直觀的:調(diào)用?list
?即可。我們沒有考慮到的是,如何使 Lisp 將列表視為代碼。這之間缺少的一環(huán)是函數(shù)?eval
,它接受一個(gè)表達(dá)式,將其求值,然后返回它的值:
> (eval '(+ 1 2 3))
6
> (eval '(format t "Hello"))
Hello
NIL
如果這看起很熟悉的話,這是應(yīng)該的。這就是我們一直交談的那個(gè)?eval
?。下面這個(gè)函數(shù)實(shí)現(xiàn)了與頂層非常相似的東西:
(defun our-toplevel ()
(do ()
(nil)
(format t "~%> ")
(print (eval (read)))))
也是因?yàn)檫@個(gè)原因,頂層也稱為讀取─求值─打印循環(huán)?(read-eval-print loop, REPL)。
調(diào)用?eval
?是跨越代碼與列表界線的一種方法。但它不是一個(gè)好方法:
eval
?處理的是原始列表 (raw list),或者當(dāng)下編譯它,或者用直譯器求值。兩種方法都比執(zhí)行編譯過的代碼來得慢許多。let
?里調(diào)用?eval
?,傳給?eval
?的表達(dá)式將無法引用由?let
?所設(shè)置的變量。有許多更好的方法 (下一節(jié)敘述)來利用產(chǎn)生代碼的這個(gè)可能性。當(dāng)然?eval
?也是有用的,唯一合法的用途像是在頂層循環(huán)使用它。
對(duì)于程序員來說,?eval
?的主要價(jià)值大概是作為 Lisp 的概念模型。我們可以想像 Lisp 是由一個(gè)長(zhǎng)的?cond
?表達(dá)式定義而成:
(defun eval (expr env)
(cond ...
((eql (car expr) 'quote) (cdr expr))
...
(t (apply (symbol-function (car expr))
(mapcar #'(lambda (x)
(eval x env))
(cdr expr))))))
許多表達(dá)式由預(yù)設(shè)子句 (default clause)來處理,預(yù)設(shè)子句獲得?car
?所引用的函數(shù),將?cdr
?所有的參數(shù)求值,并返回將前者應(yīng)用至后者的結(jié)果。?[1]
但是像?(quote?x)
?那樣的句子就不能用這樣的方式來處理,因?yàn)?quote
?就是為了防止它的參數(shù)被求值而存在的。所以我們需要給quote
?寫一個(gè)特別的子句。這也是為什么本質(zhì)上將其稱為特殊操作符 (special operator): 一個(gè)需要被實(shí)現(xiàn)為?eval
?的一個(gè)特殊情況的操作符。
函數(shù)?coerce
?與?compile
?提供了一個(gè)類似的橋梁,讓你把列表轉(zhuǎn)成代碼。你可以?coerce
?一個(gè) lambda 表達(dá)式,使其成為函數(shù),
> (coerce '(lambda (x) x) 'function)
#<Interpreted-Function BF9D96>
而如果你將?nil
?作為第一個(gè)參數(shù)傳給?compile
?,它會(huì)編譯作為第二個(gè)參數(shù)傳入的 lambda 表達(dá)式。
> (compile nil '(lambda (x) (+ x 2)))
#<Compiled-Function BF55BE>
NIL
NIL
由于?coerce
?與?compile
?可接受列表作為參數(shù),一個(gè)程序可以在動(dòng)態(tài)執(zhí)行時(shí) (on the fly)構(gòu)造新函數(shù)。但與調(diào)用?eval
?比起來,這不是一個(gè)從根本解決的辦法,并且需抱有同樣的疑慮來檢視這兩個(gè)函數(shù)。
函數(shù)?eval
?,?coerce
?與?compile
?的麻煩不是它們跨越了代碼與列表之間的界線,而是它們?cè)趫?zhí)行期做這件事??缭浇缇€的代價(jià)昂貴。大多數(shù)情況下,在編譯期做這件事是沒問題的,當(dāng)你的程序執(zhí)行時(shí),幾乎不用成本。下一節(jié)會(huì)示范如何辦到這件事。
寫出能寫程序的程序的最普遍方法是通過定義宏。宏是通過轉(zhuǎn)換 (transformation)而實(shí)現(xiàn)的操作符。你通過說明你一個(gè)調(diào)用應(yīng)該要翻譯成什么,來定義一個(gè)宏。這個(gè)翻譯稱為宏展開(macro-expansion),宏展開由編譯器自動(dòng)完成。所以宏所產(chǎn)生的代碼,會(huì)變成程序的一個(gè)部分,就像你自己輸入的程序一樣。
宏通常通過調(diào)用?defmacro
?來定義。一個(gè)?defmacro
?看起來很像?defun
?。但是與其定義一個(gè)函數(shù)調(diào)用應(yīng)該產(chǎn)生的值,它定義了該怎么翻譯出一個(gè)函數(shù)調(diào)用。舉例來說,一個(gè)將其參數(shù)設(shè)為?nil
?的宏可以定義成如下:
(defmacro nil! (x)
(list 'setf x nil))
這定義了一個(gè)新的操作符,稱為?nil!
?,它接受一個(gè)參數(shù)。一個(gè)這樣形式?(nil!?a)
?的調(diào)用,會(huì)在求值或編譯前,被翻譯成?(setf?anil)
?。所以如果我們輸入?(nil!?x)
?至頂層,
> (nil! x)
NIL
> x
NIL
完全等同于輸入表達(dá)式?(setf?x?nil)
?。
要測(cè)試一個(gè)函數(shù),我們調(diào)用它,但要測(cè)試一個(gè)宏,我們看它的展開式 (expansion)。
函數(shù)?macroexpand-1
?接受一個(gè)宏調(diào)用,并產(chǎn)生它的展開式:
> (macroexpand-1 '(nil! x))
(SETF X NIL)
T
一個(gè)宏調(diào)用可以展開成另一個(gè)宏調(diào)用。當(dāng)編譯器(或頂層)遇到一個(gè)宏調(diào)用時(shí),它持續(xù)展開它,直到不可展開為止。
理解宏的秘密是理解它們是如何被實(shí)現(xiàn)的。在臺(tái)面底下,它們只是轉(zhuǎn)換成表達(dá)式的函數(shù)。舉例來說,如果你傳入這個(gè)形式?(nil!?a)
的表達(dá)式給這個(gè)函數(shù)
(lambda (expr)
(apply #'(lambda (x) (list 'setf x nil))
(cdr expr)))
它會(huì)返回?(setf?a?nil)
?。當(dāng)你使用?defmacro
?,你定義一個(gè)類似這樣的函數(shù)。?macroexpand-1
?全部所做的事情是,當(dāng)它看到一個(gè)表達(dá)式的?car
?是宏時(shí),將表達(dá)式傳給對(duì)應(yīng)的函數(shù)。
反引號(hào)讀取宏 (read-macro)使得從模版 (templates)建構(gòu)列表變得有可能。反引號(hào)廣泛使用在宏定義中。一個(gè)平常的引用是鍵盤上的右引號(hào) (apostrophe),然而一個(gè)反引號(hào)是一個(gè)左引號(hào)。(譯注: open quote 左引號(hào),closed quote 右引號(hào))。它稱作“反引號(hào)”是因?yàn)樗雌饋硐袷欠催^來的引號(hào) (titled backwards)。
(譯注: 反引號(hào)是鍵盤左上方數(shù)字 1 左邊那個(gè):?````?,而引號(hào)是 enter 左邊那個(gè)?
'`)
一個(gè)反引號(hào)單獨(dú)使用時(shí),等于普通的引號(hào):
> `(a b c)
(A B C)
和普通引號(hào)一樣,單一個(gè)反引號(hào)保護(hù)其參數(shù)被求值。
反引號(hào)的優(yōu)點(diǎn)是,在一個(gè)反引號(hào)表達(dá)式里,你可以使用?,
?(逗號(hào))與?,@
?(comma-at)來重啟求值。如果你在反引號(hào)表達(dá)式里,在某個(gè)東西前面加逗號(hào),則它會(huì)被求值。所以我們可以使用反引號(hào)與逗號(hào)來建構(gòu)列表模版:
> (setf a 1 b 2)
2
> `(a is ,a and b is ,b)
(A IS 1 AND B IS 2)
通過使用反引號(hào)取代調(diào)用?list
?,我們可以寫出會(huì)產(chǎn)生出展開式的宏。舉例來說?nil!
?可以定義為:
(defmacro nil! (x)
`(setf ,x nil))
,@
?與逗號(hào)相似,但將(本來應(yīng)該是列表的)參數(shù)扒開。將列表的元素插入模版來取代列表。
> (setf lst '(a b c))
(A B C)
> `(lst is ,lst)
(LST IS (A B C))
> `(its elements are ,@lst)
(ITS ELEMENTS ARE A B C)
,@
?在宏里很有用,舉例來說,在用剩余參數(shù)表示代碼主體的宏。假設(shè)我們想要一個(gè)?while
?宏,只要初始測(cè)試表達(dá)式為真,對(duì)其主體求值:
> (let ((x 0))
(while (< x 10)
(princ x)
(incf x)))
0123456789
NIL
我們可以通過使用一個(gè)剩余參數(shù) (rest parameter) ,搜集主體的表達(dá)式列表,來定義一個(gè)這樣的宏,接著使用 comma-at 來扒開這個(gè)列表放至展開式里:
(defmacro while (test &rest body)
`(do ()
((not ,test))
,@body))
圖 10.1 包含了重度依賴宏的一個(gè)示例函數(shù) ── 一個(gè)使用快速排序演算法?λ?來排序向量的函數(shù)。這個(gè)函數(shù)的工作方式如下:
(defun quicksort (vec l r)
(let ((i l)
(j r)
(p (svref vec (round (+ l r) 2)))) ; 1
(while (<= i j) ; 2
(while (< (svref vec i) p) (incf i))
(while (> (svref vec j) p) (decf j))
(when (<= i j)
(rotatef (svref vec i) (svref vec j))
(incf i)
(decf j)))
(if (>= (- j l) 1) (quicksort vec l j)) ; 3
(if (>= (- r i) 1) (quicksort vec i r)))
vec)
圖 10.1 快速排序。
每一次遞歸時(shí),分割越變?cè)叫?,直到向量完整排序?yàn)橹埂?/p>
在圖 10.1 的實(shí)現(xiàn)里,接受一個(gè)向量以及標(biāo)記欲排序范圍的兩個(gè)整數(shù)。這個(gè)范圍當(dāng)下的中間元素被選為主鍵 (?p
?)。接著從左右兩端開始產(chǎn)生分割,并將左邊太大或右邊太小的元素交換過來。(將兩個(gè)參數(shù)傳給?rotatef
?函數(shù),交換它們的值。)最后,如果一個(gè)分割含有多個(gè)元素時(shí),用同樣的流程來排序它們。
除了我們前一節(jié)定義的?while
?宏之外,圖 10.1 也用了內(nèi)置的?when
?,?incf
?,?decf
?以及?rotatef
?宏。使用這些宏使程序看起來更加簡(jiǎn)潔與清晰。
撰寫宏是一種獨(dú)特的程序設(shè)計(jì),它有著獨(dú)一無二的目標(biāo)與問題。能夠改變編譯器所看到的東西,就像是能夠重寫它一樣。所以當(dāng)你開始撰寫宏時(shí),你需要像語言設(shè)計(jì)者一樣思考。
本節(jié)快速給出宏所牽涉問題的概要,以及解決它們的技巧。作為一個(gè)例子,我們會(huì)定義一個(gè)稱為?ntimes
?的宏,它接受一個(gè)數(shù)字?n?并對(duì)其主體求值?n?次。
> (ntimes 10
(princ "."))
..........
NIL
下面是一個(gè)不正確的?ntimes
?定義,說明了宏設(shè)計(jì)中的某些議題:
(defmacro ntimes (n &rest body)
`(do ((x 0 (+ x 1)))
((>= x ,n))
,@body))
這個(gè)定義第一眼看起來可能沒問題。在上面這個(gè)情況,它會(huì)如預(yù)期的工作。但實(shí)際上它在兩個(gè)方面壞掉了。
一個(gè)宏設(shè)計(jì)者需要考慮的問題之一是,不小心引入的變量捕捉 (variable capture)。這發(fā)生在當(dāng)一個(gè)在宏展開式里用到的變量,恰巧與展開式即將插入的語境里,有使用同樣名字作為變量的情況。不正確的?ntimes
?定義創(chuàng)造了一個(gè)變量?x
?。所以如果這個(gè)宏在已經(jīng)有x
?作為名字的地方被調(diào)用時(shí),它可能無法做到我們所預(yù)期的:
> (let ((x 10))
(ntimes 5
(setf x (+ x 1)))
x)
10
如果?ntimes
?如我們預(yù)期般的執(zhí)行,這個(gè)表達(dá)式應(yīng)該會(huì)對(duì)?x
?遞增五次,最后返回?15
?。但因?yàn)楹暾归_剛好使用?x
?作為迭代變量,setf
?表達(dá)式遞增那個(gè)?x
?,而不是我們要遞增的那個(gè)。一旦宏調(diào)用被展開,前述的展開式變成:
> (let ((x 10))
(do ((x 0 (+ x 1)))
((>= x 5))
(setf x (+ x 1)))
x)
最普遍的解法是不要使用任何可能會(huì)被捕捉的一般符號(hào)。取而代之的我們使用 gensym (8.4 小節(jié))。因?yàn)?read
?函數(shù)?intern
?每個(gè)它見到的符號(hào),所以在一個(gè)程序里,沒有可能會(huì)有任何符號(hào)會(huì)?eql
?gensym。如果我們使用 gensym 而不是?x
?來重寫?ntimes
?的定義,至少對(duì)于變量捕捉來說,它是安全的:
(defmacro ntimes (n &rest body)
(let ((g (gensym)))
`(do ((,g 0 (+ ,g 1)))
((>= ,g ,n))
,@body)))
但這個(gè)宏在另一問題上仍有疑慮: 多重求值 (multiple evaluation)。因?yàn)榈谝粋€(gè)參數(shù)被直接插入?do
?表達(dá)式,它會(huì)在每次迭代時(shí)被求值。當(dāng)?shù)谝粋€(gè)參數(shù)是有副作用的表達(dá)式,這個(gè)錯(cuò)誤非常清楚地表現(xiàn)出來:
> (let ((v 10))
(ntimes (setf v (- v 1))
(princ ".")))
.....
NIL
由于?v
?一開始是?10
?,而?setf
?返回其第二個(gè)參數(shù)的值,應(yīng)該印出九個(gè)句點(diǎn)。實(shí)際上它只印出五個(gè)。
如果我們看看宏調(diào)用所展開的表達(dá)式,就可以知道為什么:
> (let ((v 10))
(do ((#:g1 0 (+ #:g1 1)))
((>= #:g1 (setf v (- v 1))))
(princ ".")))
每次迭代我們不是把迭代變量 (gensym 通常印出前面有?#:
?的符號(hào))與?9
?比較,而是與每次求值時(shí)會(huì)遞減的表達(dá)式比較。這如同每次我們查看地平線時(shí),地平線都越來越近。
避免非預(yù)期的多重求值的方法是設(shè)置一個(gè)變量,在任何迭代前將其設(shè)為有疑惑的那個(gè)表達(dá)式。這通常牽扯到另一個(gè) gensym:
(defmacro ntimes (n &rest body)
(let ((g (gensym))
(h (gensym)))
`(let ((,h ,n))
(do ((,g 0 (+ ,g 1)))
((>= ,g ,h))
,@body))))
終于,這是一個(gè)?ntimes
?的正確定義。
非預(yù)期的變量捕捉與多重求值是折磨宏的主要問題,但不只有這些問題而已。有經(jīng)驗(yàn)后,要避免這樣的錯(cuò)誤與避免更熟悉的錯(cuò)誤一樣簡(jiǎn)單,比如除以零的錯(cuò)誤。
你的 Common Lisp 實(shí)現(xiàn)是一個(gè)學(xué)習(xí)更多有關(guān)宏的好地方。借由調(diào)用展開至內(nèi)置宏,你可以理解它們是怎么寫的。下面是大多數(shù)實(shí)現(xiàn)對(duì)于一個(gè)?cond
?表達(dá)式會(huì)產(chǎn)生的展開式:
> (pprint (macroexpand-1 '(cond (a b)
(c d e)
(t f))))
(IF A
B
(IF C
(PROGN D E)
F))
函數(shù)?pprint
?印出像代碼一樣縮排的表達(dá)式,這在檢視宏展開式時(shí)特別有用。
由于一個(gè)宏調(diào)用可以直接在它出現(xiàn)的地方展開成代碼,任何展開為?setf
?表達(dá)式的宏調(diào)用都可以作為?setf
?表達(dá)式的第一個(gè)參數(shù)。 舉例來說,如果我們定義一個(gè)?car
?的同義詞,
(defmacro cah (lst) `(car ,lst))
然后因?yàn)橐粋€(gè)?car
?調(diào)用可以是?setf
?的第一個(gè)參數(shù),而?cah
?一樣可以:
> (let ((x (list 'a 'b 'c)))
(setf (cah x) 44)
x)
(44 B C)
撰寫一個(gè)展開成一個(gè)?setf
?表達(dá)式的宏是另一個(gè)問題,是一個(gè)比原先看起來更為困難的問題??雌饋硪苍S你可以這樣實(shí)現(xiàn)?incf
?,只要
(defmacro incf (x &optional (y 1)) ; wrong
`(setf ,x (+ ,x ,y)))
但這是行不通的。這兩個(gè)表達(dá)式不相等:
(setf (car (push 1 lst)) (1+ (car (push 1 lst))))
(incf (car (push 1 lst)))
如果?lst
?是?nil
?的話,第二個(gè)表達(dá)式會(huì)設(shè)成?(2)
?,但第一個(gè)表達(dá)式會(huì)設(shè)成?(1?2)
?。
Common Lisp 提供了?define-modify-macro
?作為寫出對(duì)于?setf
?限制類別的宏的一種方法 它接受三個(gè)參數(shù): 宏的名字,額外的參數(shù) (隱含第一個(gè)參數(shù)?place
),以及產(chǎn)生出?place
?新數(shù)值的函數(shù)名。所以我們可以將?incf
?定義為
(define-modify-macro our-incf (&optional (y 1)) +)
另一版將元素推至列表尾端的?push
?可寫成:
(define-modify-macro append1f (val)
(lambda (lst val) (append lst (list val))))
后者會(huì)如下工作:
> (let ((lst '(a b c)))
(append1f lst 'd)
lst)
(A B C D)
順道一提,?push
?與?pop
?都不能定義為 modify-macros,前者因?yàn)?place
?不是其第一個(gè)參數(shù),而后者因?yàn)槠浞祷刂挡皇歉暮蟮膶?duì)象。
6.4 節(jié)介紹了實(shí)用函數(shù) (utility)的概念,一種像是構(gòu)造 Lisp 的通用操作符。我們可以使用宏來定義不能寫作函數(shù)的實(shí)用函數(shù)。我們已經(jīng)見過幾個(gè)例子:?nil!
?,?ntimes
?以及?while
?,全部都需要寫成宏,因?yàn)樗鼈內(nèi)夹枰撤N控制參數(shù)求值的方法。本節(jié)給出更多你可以使用宏寫出的多種實(shí)用函數(shù)。圖 10.2 挑選了幾個(gè)實(shí)踐中證實(shí)值得寫的實(shí)用函數(shù)。
(defmacro for (var start stop &body body)
(let ((gstop (gensym)))
`(do ((,var ,start (1+ ,var))
(,gstop ,stop))
((> ,var ,gstop))
,@body)))
(defmacro in (obj &rest choices)
(let ((insym (gensym)))
`(let ((,insym ,obj))
(or ,@(mapcar #'(lambda (c) `(eql ,insym ,c))
choices)))))
(defmacro random-choice (&rest exprs)
`(case (random ,(length exprs))
,@(let ((key -1))
(mapcar #'(lambda (expr)
`(,(incf key) ,expr))
exprs))))
(defmacro avg (&rest args)
`(/ (+ ,@args) ,(length args)))
(defmacro with-gensyms (syms &body body)
`(let ,(mapcar #'(lambda (s)
`(,s (gensym)))
syms)
,@body))
(defmacro aif (test then &optional else)
`(let ((it ,test))
(if it ,then ,else)))
圖 10.2: 實(shí)用宏函數(shù)
第一個(gè)?for
?,設(shè)計(jì)上與?while
?相似 (164 頁,譯注: 10.3 節(jié))。它是給需要使用一個(gè)綁定至一個(gè)值的范圍的新變量來對(duì)主體求值的循環(huán):
> (for x 1 8
(princ x))
12345678
NIL
這比寫出等效的?do
?來得省事,
(do ((x 1 (+ x 1)))
((> x 8))
(princ x))
這非常接近實(shí)際的展開式:
(do ((x 1 (1+ x))
(#:g1 8))
((> x #:g1))
(princ x))
宏需要引入一個(gè)額外的變量來持有標(biāo)記范圍 (range)結(jié)束的值。 上面在例子里的?8
?也可是個(gè)函數(shù)調(diào)用,這樣我們就不需要求值好幾次。額外的變量需要是一個(gè) gensym ,為了避免非預(yù)期的變量捕捉。
圖 10.2 的第二個(gè)宏?in
?,若其第一個(gè)參數(shù)?eql
?任何自己其他的參數(shù)時(shí),返回真。表達(dá)式我們可以寫成:
(in (car expr) '+ '- '*)
我們可以改寫成:
(let ((op (car expr)))
(or (eql op '+)
(eql op '-)
(eql op '*)))
確實(shí),第一個(gè)表達(dá)式展開后像是第二個(gè),除了變量?op
?被一個(gè) gensym 取代了。
下一個(gè)例子?random-choice
?,隨機(jī)選取一個(gè)參數(shù)求值。在 74 頁 (譯注: 第 4 章的圖 4.6)我們需要隨機(jī)在兩者之間選擇。?random-choice
?宏實(shí)現(xiàn)了通用的解法。一個(gè)像是這樣的調(diào)用:
(random-choice (turn-left) (turn-right))
會(huì)被展開為:
(case (random 2)
(0 (turn-left))
(1 (turn-right)))
下一個(gè)宏?with-gensyms
?主要預(yù)期用在宏主體里。它不尋常,特別是在特定應(yīng)用中的宏,需要 gensym 幾個(gè)變量。有了這個(gè)宏,與其
(let ((x (gensym)) (y (gensym)) (z (gensym)))
...)
我們可以寫成
(with-gensyms (x y z)
...)
到目前為止,圖 10.2 定義的宏,沒有一個(gè)可以定義成函數(shù)。作為一個(gè)規(guī)則,寫成宏是因?yàn)槟悴荒軐⑺鼘懗珊瘮?shù)。但這個(gè)規(guī)則有幾個(gè)例外。有時(shí)候你或許想要定義一個(gè)操作符來作為宏,好讓它在編譯期完成它的工作。宏?avg
?返回其參數(shù)的平均值,
> (avg 2 4 8)
14/3
是一個(gè)這種例子的宏。我們可以將?avg
?寫成函數(shù),
(defun avg (&rest args)
(/ (apply #'+ args) (length args)))
但它會(huì)需要在執(zhí)行期找出參數(shù)的數(shù)量。只要我們?cè)敢夥艞墤?yīng)用?avg
?,為什么不在編譯期調(diào)用?length
?呢?
圖 10.2 的最后一個(gè)宏是?aif
?,它在此作為一個(gè)故意變量捕捉的例子。它讓我們可以使用變量?it
?來引用到一個(gè)條件式里的測(cè)試參數(shù)所返回的值。也就是說,與其寫成
(let ((val (calculate-something)))
(if val
(1+ val)
0))
我們可以寫成
(aif (calculate-something)
(1+ it)
0)
小心使用?(?Use judiciously),預(yù)期的變量捕捉可以是一個(gè)無價(jià)的技巧。Common Lisp 本身在多處使用它: 舉例來說?next-method-p
與?call-next-method
?皆依賴于變量捕捉。
像這些宏明確演示了為何要撰寫替你寫程序的程序。一旦你定義了?for
?,你就不需要寫整個(gè)?do
?表達(dá)式。值得寫一個(gè)宏只為了節(jié)省打字嗎?非常值得。節(jié)省打字是程序設(shè)計(jì)的全部;一個(gè)編譯器的目的便是替你省下使用機(jī)械語言輸入程序的時(shí)間。而宏允許你將同樣的優(yōu)點(diǎn)帶到特定的應(yīng)用里,就像高階語言帶給程序語言一般。通過審慎的使用宏,你也許可以使你的程序比起原來大幅度地精簡(jiǎn),并使程序更顯著地容易閱讀、撰寫及維護(hù)。
如果仍對(duì)此懷疑,考慮看看如果你沒有使用任何內(nèi)置宏時(shí),程序看起來會(huì)是怎么樣。所有宏產(chǎn)生的展開式,你會(huì)需要用手產(chǎn)生。你也可以將這個(gè)問題用在另一方面。當(dāng)你在撰寫一個(gè)程序時(shí),捫心自問,我需要撰寫宏展開式嗎?如果是的話,宏所產(chǎn)生的展開式就是你需要寫的東西。
現(xiàn)在宏已經(jīng)介紹過了,我們看過更多的 Lisp 是由超乎我們想像的 Lisp 寫成。許多不是函數(shù)的 Common Lisp 操作符是宏,而他們?nèi)坑?Lisp 寫成的。只有二十五個(gè) Common Lisp 內(nèi)置的操作符是特殊操作符。
John Foderaro?將 Lisp 稱為“可程序的程序語言?!?λ?通過撰寫你自己的函數(shù)與宏,你將 Lisp 變成任何你想要的語言。 (我們會(huì)在 17 章看到這個(gè)可能性的圖形化示范)無論你的程序適合何種形式,你確信你可以將 Lisp 塑造成適合它的語言。
宏是這個(gè)靈活性的主要成分之一。它們?cè)试S你將 Lisp 變得完全認(rèn)不出來,但仍然用一種有原則且高效的方法來實(shí)作。在 Lisp 社區(qū)里,宏是個(gè)越來越感興趣的主題。可以使用宏辦到驚人之事是很清楚的,但更確信的是宏背后還有更多需要被探索。如果你想的話,可以通過你來發(fā)現(xiàn)。Lisp 永遠(yuǎn)將進(jìn)化放在程序員手里。這是它為什么存活的原因。
eval
?是讓 Lisp 將列表視為代碼的一種方法,但這是不必要而且效率低落的。setf
?表達(dá)式的問題。x
?是?a
?,?y
?是?b
?以及?z
?是?(c?d)
?,寫出反引用表達(dá)式僅包含產(chǎn)生下列結(jié)果之一的變量:(a) ((C D) A Z)
(b) (X B C D)
(c) ((C D A) Z)
cond
?來定義?if
?。> (let ((n 2))
(nth-expr n (/ 1 0) (+ 1 2) (/ 1 0)))
3
ntimes
?(167 頁,譯注: 10.5 節(jié))使其展開成一個(gè) (區(qū)域)遞歸函數(shù),而不是一個(gè)?do
?表達(dá)式。n-of
?,接受一個(gè)數(shù)字?n?與一個(gè)表達(dá)式,返回一個(gè)?n?個(gè)漸進(jìn)值:> (let ((i 0) (n 4))
(n-of n (incf i)))
(1 2 3 4)
push
?的定義哪里錯(cuò)誤?(defmacro push (obj lst)
`(setf ,lst (cons ,obj ,lst)))
舉出一個(gè)不會(huì)與實(shí)際 push 做一樣事情的函數(shù)調(diào)用例子。
> (let ((x 1))
(double x)
x)
2
腳注
[1] | 要真的復(fù)制一個(gè) Lisp 的話,?eval
?會(huì)需要接受第二個(gè)參數(shù) (這里的?env
) 來表示詞法環(huán)境 (lexical enviroment)。這個(gè)模型的eval
?是不正確的,因?yàn)樗趯?duì)參數(shù)求值前就取出函數(shù),然而 Common Lisp 故意沒有特別指出這兩個(gè)操作的順序。
更多建議: