第八章 宏
===========
用戶可以通過定義宏來創建屬于自己的`special form`。宏是一個具有與它相關聯的轉換器程序的標記。當Scheme遇到一個宏表達式,即以macro—作為開頭的列表時,它會將宏的轉換器應用于宏表達式中的子列表,而且會對最后的轉換結果進行求值。
理想情況下,“宏”指代從一種代碼文本到另一種代碼文本的純文本變換。這種變換對于縮寫那些復雜的但經常出現的文本模式十分有用。
宏通過`define-macro`來定義(見附錄A.3)。例如,如果你的Scheme缺少條件表達式when,你就可以以下述宏定義when:
```scheme
(define-macro when
(lambda (test . branch)
(list 'if test
(cons 'begin branch))))
```
這樣定義的when轉換器能夠把一個when表達式轉換為等價的if表達式。用這個宏,下面的when表達式
```scheme
(when (< (pressure tube) 60)
(open-valve tube)
(attach floor-pump tube)
(depress floor-pump 5)
(detach floor-pump tube)
(close-valve tube))
```
將會被轉換為另一個表達式,把when轉換器應用到when表達式的子`form`:
```scheme
(apply
(lambda (test . branch)
(list 'if test
(cons 'begin branch)))
'((< (pressure tube) 60)
(open-valve tube)
(attach floor-pump tube)
(depress floor-pump 5)
(detach floor-pump tube)
(close-valve tube)))
```
這個轉換產生了一個列表:
```scheme
(if (< (pressure tube) 60)
(begin
(open-valve tube)
(attach floor-pump tube)
(depress floor-pump 5)
(detach floor-pump tube)
(close-valve tube)))
```
Scheme將會對這個表達式進行求值,就像它對其他表達式所做的一樣。
再來看另一個例子,這有一個`unless`(`when`的另一種形式)的宏定義:
```scheme
(define-macro unless
(lambda (test . branch)
(list 'if
(list 'not test)
(cons 'begin branch))))
```
另外,我們可以調用`when`放進`unless`定義中:
```scheme
(define-macro unless
(lambda (test . branch)
(cons 'when
(cons (list 'not test) branch))))
```
宏表達式可以引用其他的宏。
## 8.1 指定一個擴展為模板
宏轉換器一般接受一些S表達式作為參數,同時產生可以被作為`form`使用的S表達式。通常情況下輸出是一個列表。在我們的when例子中,使用下面語句創建輸出列表:
```scheme
(list 'if test
(cons 'begin branch))
```
其中test與宏的第一個子`form`綁定,即:
```scheme
(< (pressure tube) 60)
```
同時`branch`與余下的宏的子`form`綁定,即:
```scheme
((open-valve tube)
(attach floor-pump tube)
(depress floor-pump 5)
(detach floor-pump tube)
(close-valve tube))
```
輸出列表可能會變得相當復雜。我們很容易能夠發現比when更加龐大的宏可以對輸出列表完成精心的加工工程。這種情況下,更方便的方法是把宏的輸出指定為模板,對宏的每種用法把相關參數插入到模板的適當位置。Scheme提供了backquote語法來指定這種模板。因此表達式:
```scheme
(list 'IF test
(cons 'BEGIN branch))
```
寫成這樣會更加方便:
```scheme
`(IF ,test
(BEGIN ,@branch))
```
我們能夠將`when`的宏表達式重構為:
```scheme
(define-macro when
(lambda (test . branch)
`(IF ,test
(BEGIN ,@branch))))
```
注意模板的格式,并不像早先列表的結構,而是對輸出列表的形態給出了直接的視覺指示。反引號(`)為列表引進了一個模板。除了以逗號(,)或(,@)作為前綴的元素外,模板的元素會在結果列表中逐字出現。(為了舉例,我們把模板的每一個會在結果中原封不動出現元素寫成了大寫)。
`,`和`,@`可以將宏參數插入到模板中。`,`插入的是逗號后面緊接著它的下一個表達式求值后的結果。`,@`(comma-splice)插入的是它的下一個表達式先splice再求值的結果。即:它消除了最外面的括號。(這說明被comma-splice引用的表達式必須是一個列表。)
在我們的例子中,給定`test`和`branch`的綁定值,很容易看到模板將擴展到所需的地步。
```scheme
(IF (< (pressure tube) 60)
(BEGIN
(open-valve tube)
(attach floor-pump tube)
(depress floor-pump 5)
(detach floor-pump tube)
(close-valve tube)))
```
## 8.2 避免在宏內部產生變量捕獲
一個二變量的`disjunction form`,`my-or`,可以定義為:
```scheme
(define-macro my-or
(lambda (x y)
`(if ,x ,x ,y)))
```
`my-or`帶有兩個參數并返回兩個之中第一個為真(非#f)的值。特別的,只有當第一個參數為假時才會對第二個參數求值。
```scheme
(my-or 1 2)
=> 1
(my-or #f 2)
=> 2
```
上述的`my-or`宏時會有一個問題。如果第一個參數為真,會重新求值第一個參數:第一次是在if語句中,第二次在then分支。如果第一個參數包含副作用,這會造成意外的結果,例如:
```scheme
(my-or
(begin
(display "doing first argument")
(newline)
#t)
2)
```
會顯示`doing first argument`兩次。
這個情況可以通過在局部變量中儲存if測試結果來避免:
```scheme
(define-macro my-or
(lambda (x y)
`(let ((temp ,x))
(if temp temp ,y))))
```
這樣基本上OK了,除非當第二個參數在宏定義中使用時包含相同的temp。例如:
```scheme
(define temp 3)
(my-or #f temp)
=> #f
```
當然結果應該是3!錯誤產生的原因是由于宏使用了局部變量`temp`儲存第一個參數(`#f`)的值,而第二個參數中的變量`temp`被宏引入的`temp`所捕獲。
```scheme
(define temp 3)
(let ((temp #f))
(if temp temp 3))
```
為避免這類錯誤,我們在選擇宏定義中的局部變量時需要小心行事。我們應該為這些變量選擇古怪的名字并熱切希望沒有人會跟它們扯上關系。例如:
```scheme
(define-macro my-or
(lambda (x y)
`(let ((+temp ,x))
(if +temp +temp ,y))))
```
如果默認+temp在宏之外的代碼中不被使用,則它就是正確的。但這種幻想是遲早要破滅的。
一個更加可靠詳細的方法就是生成保證不會被其他方式占用的符號。當調用`gensym`程序時,它會產生出獨一無二的標志。這是一個使用`gensym`的`my-or`的安全定義:
```scheme
(define-macro my-or
(lambda (x y)
(let ((temp (gensym)))
`(let ((,temp ,x))
(if ,temp ,temp ,y)))))
```
為了簡明,在本文中定義的宏,不使用`gensym`方法。相反,我們將假設變量捕獲這個問題已經被考慮到了,而使用更加簡明的`+`作為前綴。我們把這些將加號開頭的標識符轉換為gensym的工作留給敏銳的讀者。
## 8.3 fluid-let
這有一個更加復雜的宏的定義,`fluid-let`(見5.2節)。`fluid-let`對一組已經存在的詞法變量指定了臨時綁定。假定一個fluid-let表達式如下:
```scheme
(fluid-let ((x 9) (y (+ y 1)))
(+ x y))
```
我們想擴展為:
```scheme
(let ((OLD-X x) (OLD-Y y))
(set! x 9)
(set! y (+ y 1))
(let ((RESULT (begin (+ x y))))
(set! x OLD-X)
(set! y OLD-Y)
RESULT))
```
在例子中我們希望標識符`OLD-X`,`OLD-Y`和`RESULT`不會捕獲`fluid-let`里的變量。
下述例子教你如何構造一個可以實施你的想法的`fluid-let`宏:
```scheme
(define-macro fluid-let
(lambda (xexe . body)
(let ((xx (map car xexe))
(ee (map cadr xexe))
(old-xx (map (lambda (ig) (gensym)) xexe))
(result (gensym)))
`(let ,(map (lambda (old-x x) `(,old-x ,x))
old-xx xx)
,@(map (lambda (x e)
`(set! ,x ,e))
xx ee)
(let ((,result (begin ,@body)))
,@(map (lambda (x old-x)
`(set! ,x ,old-x))
xx old-xx)
,result)))))
```
宏的參數是`xexe`,是由`fluid-let`引進的變量/表達式列表;而`body`,則是在`fluid-let`主體中的表達式列表。在我們的例子中,這兩者分別是`((x 9) (y (+ y 1)`和`((+ xy))`。
宏的主體引進了一堆局部變量:`xx`是從變量/表達式中提取的變量列表。`ee`是對應的表達式列表。`old-xx`是新的標識符的列表,對應于`xx`中的每個變量。這些曾用來儲存`xx`的傳入值,這樣我們可以將`xx`恢復到`fluid-let`主體求值前的狀態。Result是另一個新標志符,用來儲存`fluid-let`主體的值。在我們的例子中,`xx`是`(x y)`,`ee`是`(9(+ y 1))`。根據你的系統實現`gensym`的方式,`old-xx`會成為列表`(GEN-63 GEN-64)`,`result`會成為`GEN-65`。
在我們的例子中,由宏創建的輸出列表像這樣:
```scheme
(let ((GEN-63 x) (GEN-64 y))
(set! x 9)
(set! y (+ y 1))
(let ((GEN-65 (begin (+ x y))))
(set! x GEN-63)
(set! y GEN-64)
GEN-65))
```
這確實可以滿足我們的需求。