Lisp 代碼是由 Lisp 對象的列表來表示。2.3 節宣稱這讓 Lisp 可以寫出**可自己寫程序的程序**。本章將示范如何跨越表達式與代碼的界線。
[TOC]
## 10.1 求值 (Eval)[](http://acl.readthedocs.org/en/latest/zhCN/ch10-cn.html#eval "Permalink to this headline")
如何產生表達式是很直觀的:調用?`list`?即可。我們沒有考慮到的是,如何使 Lisp 將列表視為代碼。這之間缺少的一環是函數?`eval`,它接受一個表達式,將其求值,然后返回它的值:
~~~
> (eval '(+ 1 2 3))
6
> (eval '(format t "Hello"))
Hello
NIL
~~~
如果這看起很熟悉的話,這是應該的。這就是我們一直交談的那個?`eval`?。下面這個函數實現了與頂層非常相似的東西:
~~~
(defun our-toplevel ()
(do ()
(nil)
(format t "~%> ")
(print (eval (read)))))
~~~
也是因為這個原因,頂層也稱為**讀取─求值─打印循環**?(read-eval-print loop, REPL)。
調用?`eval`?是跨越代碼與列表界線的一種方法。但它不是一個好方法:
1. 它的效率低下:?`eval`?處理的是原始列表 (raw list),或者當下編譯它,或者用直譯器求值。兩種方法都比執行編譯過的代碼來得慢許多。
2. 表達式在沒有詞法語境 (lexical context)的情況下被求值。舉例來說,如果你在一個?`let`?里調用?`eval`?,傳給?`eval`?的表達式將無法引用由?`let`?所設置的變量。
有許多更好的方法 (下一節敘述)來利用產生代碼的這個可能性。當然?`eval`?也是有用的,唯一合法的用途像是在頂層循環使用它。
對于程序員來說,?`eval`?的主要價值大概是作為 Lisp 的概念模型。我們可以想像 Lisp 是由一個長的?`cond`?表達式定義而成:
~~~
(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))))))
~~~
許多表達式由預設子句 (default clause)來處理,預設子句獲得?`car`?所引用的函數,將?`cdr`?所有的參數求值,并返回將前者應用至后者的結果。?[[1]](http://acl.readthedocs.org/en/latest/zhCN/ch10-cn.html#id5)
但是像?`(quote?x)`?那樣的句子就不能用這樣的方式來處理,因為?`quote`?就是為了防止它的參數被求值而存在的。所以我們需要給`quote`?寫一個特別的子句。這也是為什么本質上將其稱為特殊操作符 (special operator): 一個需要被實現為?`eval`?的一個特殊情況的操作符。
函數?`coerce`?與?`compile`?提供了一個類似的橋梁,讓你把列表轉成代碼。你可以?`coerce`?一個 lambda 表達式,使其成為函數,
~~~
> (coerce '(lambda (x) x) 'function)
#<Interpreted-Function BF9D96>
~~~
而如果你將?`nil`?作為第一個參數傳給?`compile`?,它會編譯作為第二個參數傳入的 lambda 表達式。
~~~
> (compile nil '(lambda (x) (+ x 2)))
#<Compiled-Function BF55BE>
NIL
NIL
~~~
由于?`coerce`?與?`compile`?可接受列表作為參數,一個程序可以在動態執行時 (on the fly)構造新函數。但與調用?`eval`?比起來,這不是一個從根本解決的辦法,并且需抱有同樣的疑慮來檢視這兩個函數。
函數?`eval`?,?`coerce`?與?`compile`?的麻煩不是它們跨越了代碼與列表之間的界線,而是它們在執行期做這件事。跨越界線的代價昂貴。大多數情況下,在編譯期做這件事是沒問題的,當你的程序執行時,幾乎不用成本。下一節會示范如何辦到這件事。
## 10.2 宏 (Macros)[](http://acl.readthedocs.org/en/latest/zhCN/ch10-cn.html#macros "Permalink to this headline")
寫出能寫程序的程序的最普遍方法是通過定義宏。*宏*是通過轉換 (transformation)而實現的操作符。你通過說明你一個調用應該要翻譯成什么,來定義一個宏。這個翻譯稱為宏展開(macro-expansion),宏展開由編譯器自動完成。所以宏所產生的代碼,會變成程序的一個部分,就像你自己輸入的程序一樣。
宏通常通過調用?`defmacro`?來定義。一個?`defmacro`?看起來很像?`defun`?。但是與其定義一個函數調用應該產生的值,它定義了該怎么翻譯出一個函數調用。舉例來說,一個將其參數設為?`nil`?的宏可以定義成如下:
~~~
(defmacro nil! (x)
(list 'setf x nil))
~~~
這定義了一個新的操作符,稱為?`nil!`?,它接受一個參數。一個這樣形式?`(nil!?a)`?的調用,會在求值或編譯前,被翻譯成?`(setf?anil)`?。所以如果我們輸入?`(nil!?x)`?至頂層,
~~~
> (nil! x)
NIL
> x
NIL
~~~
完全等同于輸入表達式?`(setf?x?nil)`?。
要測試一個函數,我們調用它,但要測試一個宏,我們看它的展開式 (expansion)。
函數?`macroexpand-1`?接受一個宏調用,并產生它的展開式:
~~~
> (macroexpand-1 '(nil! x))
(SETF X NIL)
T
~~~
一個宏調用可以展開成另一個宏調用。當編譯器(或頂層)遇到一個宏調用時,它持續展開它,直到不可展開為止。
理解宏的秘密是理解它們是如何被實現的。在臺面底下,它們只是轉換成表達式的函數。舉例來說,如果你傳入這個形式?`(nil!?a)`的表達式給這個函數
~~~
(lambda (expr)
(apply #'(lambda (x) (list 'setf x nil))
(cdr expr)))
~~~
它會返回?`(setf?a?nil)`?。當你使用?`defmacro`?,你定義一個類似這樣的函數。?`macroexpand-1`?全部所做的事情是,當它看到一個表達式的?`car`?是宏時,將表達式傳給對應的函數。
## 10.3 反引號 (Backquote)[](http://acl.readthedocs.org/en/latest/zhCN/ch10-cn.html#backquote "Permalink to this headline")
反引號讀取宏 (read-macro)使得從模版 (templates)建構列表變得有可能。反引號廣泛使用在宏定義中。一個平常的引用是鍵盤上的右引號 (apostrophe),然而一個反引號是一個左引號。(譯注: open quote 左引號,closed quote 右引號)。它稱作“反引號”是因為它看起來像是反過來的引號 (titled backwards)。
(譯注: 反引號是鍵盤左上方數字 1 左邊那個:?`````?,而引號是 enter 左邊那個?`'`)
一個反引號單獨使用時,等于普通的引號:
~~~
> `(a b c)
(A B C)
~~~
和普通引號一樣,單一個反引號保護其參數被求值。
反引號的優點是,在一個反引號表達式里,你可以使用?`,`?(逗號)與?`,@`?(comma-at)來重啟求值。如果你在反引號表達式里,在某個東西前面加逗號,則它會被求值。所以我們可以使用反引號與逗號來建構列表模版:
~~~
> (setf a 1 b 2)
2
> `(a is ,a and b is ,b)
(A IS 1 AND B IS 2)
~~~
通過使用反引號取代調用?`list`?,我們可以寫出會產生出展開式的宏。舉例來說?`nil!`?可以定義為:
~~~
(defmacro nil! (x)
`(setf ,x nil))
~~~
`,@`?與逗號相似,但將(本來應該是列表的)參數扒開。將列表的元素插入模版來取代列表。
~~~
> (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)
~~~
`,@`?在宏里很有用,舉例來說,在用剩余參數表示代碼主體的宏。假設我們想要一個?`while`?宏,只要初始測試表達式為真,對其主體求值:
~~~
> (let ((x 0))
(while (< x 10)
(princ x)
(incf x)))
0123456789
NIL
~~~
我們可以通過使用一個剩余參數 (rest parameter) ,搜集主體的表達式列表,來定義一個這樣的宏,接著使用 comma-at 來扒開這個列表放至展開式里:
~~~
(defmacro while (test &rest body)
`(do ()
((not ,test))
,@body))
~~~
## 10.4 示例:快速排序法(Example: Quicksort)[](http://acl.readthedocs.org/en/latest/zhCN/ch10-cn.html#example-quicksort "Permalink to this headline")
圖 10.1 包含了重度依賴宏的一個示例函數 ── 一個使用快速排序演算法?[λ](http://acl.readthedocs.org/en/latest/zhCN/notes-cn.html#notes-164)?來排序向量的函數。這個函數的工作方式如下:
~~~
(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 快速排序。**
1. 開始你通過選擇某個元素作為主鍵(?*pivot*?)。許多實現選擇要被排序的序列中間元素。
2. 接著你分割(partition)向量,持續交換元素,直到所有主鍵左邊的元素小于主鍵,右邊的元素大于主鍵。
3. 最后,如果左右分割之一有兩個或更多元素時,你遞歸地應用這個算法至向量的那些分割上。
每一次遞歸時,分割越變越小,直到向量完整排序為止。
在圖 10.1 的實現里,接受一個向量以及標記欲排序范圍的兩個整數。這個范圍當下的中間元素被選為主鍵 (?`p`?)。接著從左右兩端開始產生分割,并將左邊太大或右邊太小的元素交換過來。(將兩個參數傳給?`rotatef`?函數,交換它們的值。)最后,如果一個分割含有多個元素時,用同樣的流程來排序它們。
除了我們前一節定義的?`while`?宏之外,圖 10.1 也用了內置的?`when`?,?`incf`?,?`decf`?以及?`rotatef`?宏。使用這些宏使程序看起來更加簡潔與清晰。
## 10.5 設計宏 (Macro Design)[](http://acl.readthedocs.org/en/latest/zhCN/ch10-cn.html#macro-design "Permalink to this headline")
撰寫宏是一種獨特的程序設計,它有著獨一無二的目標與問題。能夠改變編譯器所看到的東西,就像是能夠重寫它一樣。所以當你開始撰寫宏時,你需要像語言設計者一樣思考。
本節快速給出宏所牽涉問題的概要,以及解決它們的技巧。作為一個例子,我們會定義一個稱為?`ntimes`?的宏,它接受一個數字?*n*?并對其主體求值?*n*?次。
~~~
> (ntimes 10
(princ "."))
..........
NIL
~~~
下面是一個不正確的?`ntimes`?定義,說明了宏設計中的某些議題:
~~~
(defmacro ntimes (n &rest body)
`(do ((x 0 (+ x 1)))
((>= x ,n))
,@body))
~~~
這個定義第一眼看起來可能沒問題。在上面這個情況,它會如預期的工作。但實際上它在兩個方面壞掉了。
一個宏設計者需要考慮的問題之一是,不小心引入的變量捕捉 (variable capture)。這發生在當一個在宏展開式里用到的變量,恰巧與展開式即將插入的語境里,有使用同樣名字作為變量的情況。不正確的?`ntimes`?定義創造了一個變量?`x`?。所以如果這個宏在已經有`x`?作為名字的地方被調用時,它可能無法做到我們所預期的:
~~~
> (let ((x 10))
(ntimes 5
(setf x (+ x 1)))
x)
10
~~~
如果?`ntimes`?如我們預期般的執行,這個表達式應該會對?`x`?遞增五次,最后返回?`15`?。但因為宏展開剛好使用?`x`?作為迭代變量,`setf`?表達式遞增那個?`x`?,而不是我們要遞增的那個。一旦宏調用被展開,前述的展開式變成:
~~~
> (let ((x 10))
(do ((x 0 (+ x 1)))
((>= x 5))
(setf x (+ x 1)))
x)
~~~
最普遍的解法是不要使用任何可能會被捕捉的一般符號。取而代之的我們使用 gensym (8.4 小節)。因為?`read`?函數?`intern`?每個它見到的符號,所以在一個程序里,沒有可能會有任何符號會?`eql`?gensym。如果我們使用 gensym 而不是?`x`?來重寫?`ntimes`?的定義,至少對于變量捕捉來說,它是安全的:
~~~
(defmacro ntimes (n &rest body)
(let ((g (gensym)))
`(do ((,g 0 (+ ,g 1)))
((>= ,g ,n))
,@body)))
~~~
但這個宏在另一問題上仍有疑慮: 多重求值 (multiple evaluation)。因為第一個參數被直接插入?`do`?表達式,它會在每次迭代時被求值。當第一個參數是有副作用的表達式,這個錯誤非常清楚地表現出來:
~~~
> (let ((v 10))
(ntimes (setf v (- v 1))
(princ ".")))
.....
NIL
~~~
由于?`v`?一開始是?`10`?,而?`setf`?返回其第二個參數的值,應該印出九個句點。實際上它只印出五個。
如果我們看看宏調用所展開的表達式,就可以知道為什么:
~~~
> (let ((v 10))
(do ((#:g1 0 (+ #:g1 1)))
((>= #:g1 (setf v (- v 1))))
(princ ".")))
~~~
每次迭代我們不是把迭代變量 (gensym 通常印出前面有?`#:`?的符號)與?`9`?比較,而是與每次求值時會遞減的表達式比較。這如同每次我們查看地平線時,地平線都越來越近。
避免非預期的多重求值的方法是設置一個變量,在任何迭代前將其設為有疑惑的那個表達式。這通常牽扯到另一個 gensym:
~~~
(defmacro ntimes (n &rest body)
(let ((g (gensym))
(h (gensym)))
`(let ((,h ,n))
(do ((,g 0 (+ ,g 1)))
((>= ,g ,h))
,@body))))
~~~
終于,這是一個?`ntimes`?的正確定義。
非預期的變量捕捉與多重求值是折磨宏的主要問題,但不只有這些問題而已。有經驗后,要避免這樣的錯誤與避免更熟悉的錯誤一樣簡單,比如除以零的錯誤。
你的 Common Lisp 實現是一個學習更多有關宏的好地方。借由調用展開至內置宏,你可以理解它們是怎么寫的。下面是大多數實現對于一個?`cond`?表達式會產生的展開式:
~~~
> (pprint (macroexpand-1 '(cond (a b)
(c d e)
(t f))))
(IF A
B
(IF C
(PROGN D E)
F))
~~~
函數?`pprint`?印出像代碼一樣縮排的表達式,這在檢視宏展開式時特別有用。
## 10.6 通用化引用 (Generalized Reference)[](http://acl.readthedocs.org/en/latest/zhCN/ch10-cn.html#generalized-reference "Permalink to this headline")
由于一個宏調用可以直接在它出現的地方展開成代碼,任何展開為?`setf`?表達式的宏調用都可以作為?`setf`?表達式的第一個參數。 舉例來說,如果我們定義一個?`car`?的同義詞,
~~~
(defmacro cah (lst) `(car ,lst))
~~~
然后因為一個?`car`?調用可以是?`setf`?的第一個參數,而?`cah`?一樣可以:
~~~
> (let ((x (list 'a 'b 'c)))
(setf (cah x) 44)
x)
(44 B C)
~~~
撰寫一個展開成一個?`setf`?表達式的宏是另一個問題,是一個比原先看起來更為困難的問題。看起來也許你可以這樣實現?`incf`?,只要
~~~
(defmacro incf (x &optional (y 1)) ; wrong
`(setf ,x (+ ,x ,y)))
~~~
但這是行不通的。這兩個表達式不相等:
~~~
(setf (car (push 1 lst)) (1+ (car (push 1 lst))))
(incf (car (push 1 lst)))
~~~
如果?`lst`?是?`nil`?的話,第二個表達式會設成?`(2)`?,但第一個表達式會設成?`(1?2)`?。
Common Lisp 提供了?`define-modify-macro`?作為寫出對于?`setf`?限制類別的宏的一種方法 它接受三個參數: 宏的名字,額外的參數 (隱含第一個參數?`place`),以及產生出?`place`?新數值的函數名。所以我們可以將?`incf`?定義為
~~~
(define-modify-macro our-incf (&optional (y 1)) +)
~~~
另一版將元素推至列表尾端的?`push`?可寫成:
~~~
(define-modify-macro append1f (val)
(lambda (lst val) (append lst (list val))))
~~~
后者會如下工作:
~~~
> (let ((lst '(a b c)))
(append1f lst 'd)
lst)
(A B C D)
~~~
順道一提,?`push`?與?`pop`?都不能定義為 modify-macros,前者因為?`place`?不是其第一個參數,而后者因為其返回值不是更改后的對象。
## 10.7 示例:實用的宏函數 (Example: Macro Utilities)[](http://acl.readthedocs.org/en/latest/zhCN/ch10-cn.html#example-macro-utilities "Permalink to this headline")
6.4 節介紹了實用函數 (utility)的概念,一種像是構造 Lisp 的通用操作符。我們可以使用宏來定義不能寫作函數的實用函數。我們已經見過幾個例子:?`nil!`?,?`ntimes`?以及?`while`?,全部都需要寫成宏,因為它們全都需要某種控制參數求值的方法。本節給出更多你可以使用宏寫出的多種實用函數。圖 10.2 挑選了幾個實踐中證實值得寫的實用函數。
~~~
(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: 實用宏函數**
第一個?`for`?,設計上與?`while`?相似 (164 頁,譯注: 10.3 節)。它是給需要使用一個綁定至一個值的范圍的新變量來對主體求值的循環:
~~~
> (for x 1 8
(princ x))
12345678
NIL
~~~
這比寫出等效的?`do`?來得省事,
~~~
(do ((x 1 (+ x 1)))
((> x 8))
(princ x))
~~~
這非常接近實際的展開式:
~~~
(do ((x 1 (1+ x))
(#:g1 8))
((> x #:g1))
(princ x))
~~~
宏需要引入一個額外的變量來持有標記范圍 (range)結束的值。 上面在例子里的?`8`?也可是個函數調用,這樣我們就不需要求值好幾次。額外的變量需要是一個 gensym ,為了避免非預期的變量捕捉。
圖 10.2 的第二個宏?`in`?,若其第一個參數?`eql`?任何自己其他的參數時,返回真。表達式我們可以寫成:
~~~
(in (car expr) '+ '- '*)
~~~
我們可以改寫成:
~~~
(let ((op (car expr)))
(or (eql op '+)
(eql op '-)
(eql op '*)))
~~~
確實,第一個表達式展開后像是第二個,除了變量?`op`?被一個 gensym 取代了。
下一個例子?`random-choice`?,隨機選取一個參數求值。在 74 頁 (譯注: 第 4 章的圖 4.6)我們需要隨機在兩者之間選擇。?`random-choice`?宏實現了通用的解法。一個像是這樣的調用:
~~~
(random-choice (turn-left) (turn-right))
~~~
會被展開為:
~~~
(case (random 2)
(0 (turn-left))
(1 (turn-right)))
~~~
下一個宏?`with-gensyms`?主要預期用在宏主體里。它不尋常,特別是在特定應用中的宏,需要 gensym 幾個變量。有了這個宏,與其
~~~
(let ((x (gensym)) (y (gensym)) (z (gensym)))
...)
~~~
我們可以寫成
~~~
(with-gensyms (x y z)
...)
~~~
到目前為止,圖 10.2 定義的宏,沒有一個可以定義成函數。作為一個規則,寫成宏是因為你不能將它寫成函數。但這個規則有幾個例外。有時候你或許想要定義一個操作符來作為宏,好讓它在編譯期完成它的工作。宏?`avg`?返回其參數的平均值,
~~~
> (avg 2 4 8)
14/3
~~~
是一個這種例子的宏。我們可以將?`avg`?寫成函數,
~~~
(defun avg (&rest args)
(/ (apply #'+ args) (length args)))
~~~
但它會需要在執行期找出參數的數量。只要我們愿意放棄應用?`avg`?,為什么不在編譯期調用?`length`?呢?
圖 10.2 的最后一個宏是?`aif`?,它在此作為一個故意變量捕捉的例子。它讓我們可以使用變量?`it`?來引用到一個條件式里的測試參數所返回的值。也就是說,與其寫成
~~~
(let ((val (calculate-something)))
(if val
(1+ val)
0))
~~~
我們可以寫成
~~~
(aif (calculate-something)
(1+ it)
0)
~~~
**小心使用**?(?*Use judiciously*),預期的變量捕捉可以是一個無價的技巧。Common Lisp 本身在多處使用它: 舉例來說?`next-method-p`與?`call-next-method`?皆依賴于變量捕捉。
像這些宏明確演示了為何要撰寫替你寫程序的程序。一旦你定義了?`for`?,你就不需要寫整個?`do`?表達式。值得寫一個宏只為了節省打字嗎?非常值得。節省打字是程序設計的全部;一個編譯器的目的便是替你省下使用機械語言輸入程序的時間。而宏允許你將同樣的優點帶到特定的應用里,就像高階語言帶給程序語言一般。通過審慎的使用宏,你也許可以使你的程序比起原來大幅度地精簡,并使程序更顯著地容易閱讀、撰寫及維護。
如果仍對此懷疑,考慮看看如果你沒有使用任何內置宏時,程序看起來會是怎么樣。所有宏產生的展開式,你會需要用手產生。你也可以將這個問題用在另一方面。當你在撰寫一個程序時,捫心自問,我需要撰寫宏展開式嗎?如果是的話,宏所產生的展開式就是你需要寫的東西。
## 10.8 源自 Lisp (On Lisp)[](http://acl.readthedocs.org/en/latest/zhCN/ch10-cn.html#lisp-on-lisp "Permalink to this headline")
現在宏已經介紹過了,我們看過更多的 Lisp 是由超乎我們想像的 Lisp 寫成。許多不是函數的 Common Lisp 操作符是宏,而他們全部用 Lisp 寫成的。只有二十五個 Common Lisp 內置的操作符是特殊操作符。
[John Foderaro](http://www.franz.com/about/bios/jkf.lhtml)?將 Lisp 稱為“可程序的程序語言。”?[λ](http://acl.readthedocs.org/en/latest/zhCN/notes-cn.html#notes-173)?通過撰寫你自己的函數與宏,你將 Lisp 變成任何你想要的語言。 (我們會在 17 章看到這個可能性的圖形化示范)無論你的程序適合何種形式,你確信你可以將 Lisp 塑造成適合它的語言。
宏是這個靈活性的主要成分之一。它們允許你將 Lisp 變得完全認不出來,但仍然用一種有原則且高效的方法來實作。在 Lisp 社區里,宏是個越來越感興趣的主題。可以使用宏辦到驚人之事是很清楚的,但更確信的是宏背后還有更多需要被探索。如果你想的話,可以通過你來發現。Lisp 永遠將進化放在程序員手里。這是它為什么存活的原因。
## Chapter 10 總結 (Summary)[](http://acl.readthedocs.org/en/latest/zhCN/ch10-cn.html#chapter-10-summary "Permalink to this headline")
1. 調用?`eval`?是讓 Lisp 將列表視為代碼的一種方法,但這是不必要而且效率低落的。
2. 你通過敘說一個調用會展開成什么來定義一個宏。臺面底下,宏只是返回表達式的函數。
3. 一個使用反引號定義的主體看起來像它會產生出的展開式 (expansion)。
4. 宏設計者需要注意變量捕捉及多重求值。宏可以通過漂亮印出 (pretty-printing)來測試它們的展開式。
5. 多重求值是大多數展開成?`setf`?表達式的問題。
6. 宏比函數來得靈活,可以用來定義許多實用函數。你甚至可以使用變量捕捉來獲得好處。
7. Lisp 存活的原因是它將進化交給程序員的雙手。宏是使其可能的部分原因之一。
## Chapter 10 練習 (Exercises)[](http://acl.readthedocs.org/en/latest/zhCN/ch10-cn.html#chapter-10-exercises "Permalink to this headline")
1. 如果?`x`?是?`a`?,?`y`?是?`b`?以及?`z`?是?`(c?d)`?,寫出反引用表達式僅包含產生下列結果之一的變量:
~~~
(a) ((C D) A Z)
(b) (X B C D)
(c) ((C D A) Z)
~~~
1. 使用?`cond`?來定義?`if`?。
2. 定義一個宏,接受一個數字?*n*?,伴隨著一個或多個表達式,并返回第?*n*?個表達式的值:
~~~
> (let ((n 2))
(nth-expr n (/ 1 0) (+ 1 2) (/ 1 0)))
3
~~~
1. 定義?`ntimes`?(167 頁,譯注: 10.5 節)使其展開成一個 (區域)遞歸函數,而不是一個?`do`?表達式。
2. 定義一個宏?`n-of`?,接受一個數字?*n*?與一個表達式,返回一個?*n*?個漸進值:
~~~
> (let ((i 0) (n 4))
(n-of n (incf i)))
(1 2 3 4)
~~~
1. 定義一個宏,接受一變量列表以及一個代碼主體,并確保變量在代碼主體被求值后恢復 (revert)到原本的數值。
2. 下面這個?`push`?的定義哪里錯誤?
~~~
(defmacro push (obj lst)
`(setf ,lst (cons ,obj ,lst)))
舉出一個不會與實際 push 做一樣事情的函數調用例子。
~~~
1. 定義一個將其參數翻倍的宏:
~~~
> (let ((x 1))
(double x)
x)
2
~~~
腳注
[[1]](http://acl.readthedocs.org/en/latest/zhCN/ch10-cn.html#id2) | 要真的復制一個 Lisp 的話,?`eval`?會需要接受第二個參數 (這里的?`env`) 來表示詞法環境 (lexical enviroment)。這個模型的`eval`?是不正確的,因為它在對參數求值前就取出函數,然而 Common Lisp 故意沒有特別指出這兩個操作的順序。