第十章:宏

2018-02-24 15:50 更新

Lisp 代碼是由 Lisp 對(duì)象的列表來表示。2.3 節(jié)宣稱這讓 Lisp 可以寫出可自己寫程序的程序。本章將示范如何跨越表達(dá)式與代碼的界線。

10.1 求值 (Eval)

如何產(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è)好方法:

  1. 它的效率低下:?eval?處理的是原始列表 (raw list),或者當(dāng)下編譯它,或者用直譯器求值。兩種方法都比執(zhí)行編譯過的代碼來得慢許多。
  2. 表達(dá)式在沒有詞法語境 (lexical context)的情況下被求值。舉例來說,如果你在一個(gè)?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ì)示范如何辦到這件事。

10.2 宏 (Macros)

寫出能寫程序的程序的最普遍方法是通過定義宏。是通過轉(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ù)。

10.3 反引號(hào) (Backquote)

反引號(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.4 示例:快速排序法(Example: Quicksort)

圖 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 快速排序。

  1. 開始你通過選擇某個(gè)元素作為主鍵(?pivot?)。許多實(shí)現(xiàn)選擇要被排序的序列中間元素。
  2. 接著你分割(partition)向量,持續(xù)交換元素,直到所有主鍵左邊的元素小于主鍵,右邊的元素大于主鍵。
  3. 最后,如果左右分割之一有兩個(gè)或更多元素時(shí),你遞歸地應(yīng)用這個(gè)算法至向量的那些分割上。

每一次遞歸時(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)潔與清晰。

10.5 設(shè)計(jì)宏 (Macro Design)

撰寫宏是一種獨(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í)特別有用。

10.6 通用化引用 (Generalized Reference)

由于一個(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ì)象。

10.7 示例:實(shí)用的宏函數(shù) (Example: Macro Utilities)

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)生的展開式就是你需要寫的東西。

10.8 源自 Lisp (On Lisp)

現(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)化放在程序員手里。這是它為什么存活的原因。

Chapter 10 總結(jié) (Summary)

  1. 調(diào)用?eval?是讓 Lisp 將列表視為代碼的一種方法,但這是不必要而且效率低落的。
  2. 你通過敘說一個(gè)調(diào)用會(huì)展開成什么來定義一個(gè)宏。臺(tái)面底下,宏只是返回表達(dá)式的函數(shù)。
  3. 一個(gè)使用反引號(hào)定義的主體看起來像它會(huì)產(chǎn)生出的展開式 (expansion)。
  4. 宏設(shè)計(jì)者需要注意變量捕捉及多重求值。宏可以通過漂亮印出 (pretty-printing)來測(cè)試它們的展開式。
  5. 多重求值是大多數(shù)展開成?setf?表達(dá)式的問題。
  6. 宏比函數(shù)來得靈活,可以用來定義許多實(shí)用函數(shù)。你甚至可以使用變量捕捉來獲得好處。
  7. Lisp 存活的原因是它將進(jìn)化交給程序員的雙手。宏是使其可能的部分原因之一。

Chapter 10 練習(xí) (Exercises)

  1. 如果?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)
  1. 使用?cond?來定義?if?。
  2. 定義一個(gè)宏,接受一個(gè)數(shù)字?n?,伴隨著一個(gè)或多個(gè)表達(dá)式,并返回第?n?個(gè)表達(dá)式的值:
> (let ((n 2))
    (nth-expr n (/ 1 0) (+ 1 2) (/ 1 0)))
3
  1. 定義?ntimes?(167 頁,譯注: 10.5 節(jié))使其展開成一個(gè) (區(qū)域)遞歸函數(shù),而不是一個(gè)?do?表達(dá)式。
  2. 定義一個(gè)宏?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)
  1. 定義一個(gè)宏,接受一變量列表以及一個(gè)代碼主體,并確保變量在代碼主體被求值后恢復(fù) (revert)到原本的數(shù)值。
  2. 下面這個(gè)?push?的定義哪里錯(cuò)誤?
(defmacro push (obj lst)
  `(setf ,lst (cons ,obj ,lst)))

舉出一個(gè)不會(huì)與實(shí)際 push 做一樣事情的函數(shù)調(diào)用例子。
  1. 定義一個(gè)將其參數(shù)翻倍的宏:
> (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è)操作的順序。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)