編寫嵌套反引號的宏

一個沒事找事的例子

當在Common Lisp中定義宏的時候,常常會使用到反引號(`)。比方說,我有這麼一個函數

(defun foobar ()
  (+ 1 1)
  (+ 2 3)
  (+ 5 8)

它被調用後會返回最後一個表達式的結果——13。如果我希望在第二個表達式計算後就把結果返回給外部的調用者的話,可以用return-from

(defun foobar ()
  (+ 1 1)
  (return-from foobar (+ 2 3))
  (+ 5 8))

當然了,這屬於沒事找事,因爲完全可以把最後兩個表達式放到一個prog1(這也是沒事找事),或者直接點,把最後一個表達式刪掉來做到同樣的效果——但如果是這樣的話這篇東西就寫不下去了,所以我偏要用return-from

還有一個更加沒事找事的辦法,就是用macrolet定義一個局部的宏來代替return-from——我很想把這個新的宏叫做return,但這樣SBCL會揍我一頓,所以我只好把這個宏叫做bye(叫做exit也會被揍)

(defun foobar ()
  (macrolet ((bye (&optional value)
               `(return-from foobar ,value)))
    (+ 1 1)
    (bye (+ 2 3))
    (+ 5 8)))

如果我有另一個叫做foobaz的函數

(defun foobaz ()
  (+ 1 2)
  (+ 3 4)
  (+ 5 6))

也想要擁有bye這種想來就來想走就走的能力的話,可以依葫蘆畫瓢地包含一個macrolet

(defun foobaz ()
  (macrolet ((bye (&optional value)
               `(return-from foobaz ,value)))
    (+ 1 2)
    (bye (+ 3 4))
    (+ 5 6)))

好了,現在我覺得每次都需要在函數體內粘貼一份bye的實現代碼太麻煩了,想要減少這種重複勞作。於是乎,我打算寫一個宏來幫我複製粘貼代碼。既然要定義宏,那麼首先應當定義這個宏的名字以及用法,姑且是這麼用的吧

(with-bye foobar
  (+ 1 1)
  (bye (+ 2 3))
  (+ 5 8))

with-bye這個宏需要能夠展開成上面的手動編寫的foobar中的函數體的代碼形式,那麼with-bye的定義中,就一定會含有macrolet的代碼,同時也就含有了反引號——好了,現在要來處理嵌套的反引號了。

這篇文章有個不錯的講解,各位不妨先看看。現在,讓我來機械化地操作一遍,給出with-bye的定義。首先,要確定生成的目標代碼中,那一些部分是可變的。對於with-bye而言,return-from的第一個參數已經macrolet的函數體是可變的,那麼不妨把這兩部分先抽象爲參數

(let ((name 'foobar)
      (body '((+ 1 1) (bye (+ 2 3)) (+ 5 8))))
  `(macrolet ((bye (&optional value)
                `(return-from ,name ,value)))
     ,@body))

但這樣是不夠的,因爲name是一個在最外層綁定的,但它被放在了兩層的反引號當中,如果它只有一個前綴的逗號,那麼它就無法在外層的反引號求值的時候被替換爲目標的FOOBAR符號。因此,需要在,name之前再添加一個反引號

(let ((name 'foobar)
      (body '((+ 1 1) (bye (+ 2 3)) (+ 5 8))))
  `(macrolet ((bye (&optional value)
                `(return-from ,,name ,value)))
     ,@body))

如果你在Emacs中對上述的表達式進行求值,那麼它吐出來的結果實際上是

(MACROLET ((BYE (&OPTIONAL VALUE)
             `(RETURN-FROM ,FOOBAR ,VALUE)))
  (+ 1 1)
  (BYE (+ 2 3))
  (+ 5 8))

顯然,這還是不對。如果生成了上面這樣的代碼,那麼對於bye而言FOOBAR就是一個未綁定的符號了。之所以會這樣,是因爲

  1. name在綁定的時候輸入的是一個符號,並且

  2. name被用在了嵌套的反引號內,它會被求值兩次——第一次求值得到符號foobar,第二次則是foobar會被求值

因此,爲了對抗第二次的求值,需要給,name加上一個前綴的引號(‘),最終效果如下

(let ((name 'foobar)
      (body '((+ 1 1) (bye (+ 2 3)) (+ 5 8))))
  `(macrolet ((bye (&optional value)
                `(return-from ,',name ,value)))
     ,@body))

所以with-bye的定義是這樣的

(defmacro with-bye (name &body body)
  `(macrolet ((bye (&optional value)
                `(return-from ,',name ,value)))
     ,@body))

機械化的操作方法

我大言不慚地總結一下,剛纔的操作步驟是這樣的。首先,找出一段有規律的、需要被用宏來實現的目標代碼;然後,識別其中的可變的代碼,給這些可變的代碼的位置起一個名字(例如上文中的namebody),將它們作爲let表達式的綁定,把目標代碼裝進同一個let表達式中。此時,目標代碼被加上了一層反引號,而根據每個名字出現的位置的不同,爲它們適當地補充一個前綴的逗號;最後,如果在嵌套的反引號中出現的名字無法被求值多次——比如符號或者列表,那麼還需要給它們在第一個逗號後面插入一個引號,避免被求值兩次招致未綁定的錯誤。

一個例子

就用上面所引用的文章裏的例子好了。有一天我覺得Common Lisp中一些常用的宏的名字實在是太長了想要精簡一下——畢竟敲鍵盤也是會累的——假裝沒有自動補全的功能。我可能會定義下面這兩個宏

(defmacro d-bind (&body body)
  `(destructuring-bind ,@body))
(defmacro mv-bind (&body body)
  `(multiple-value-bind ,@body))

顯然,這裏的代碼的寫法出現了重複模式,不妨試用按照機械化的操作手法來提煉出一個宏。第一步,先識別出其中可變的內容。對於上面這個例子而言,變化的地方其實只有兩個名字——新宏的名字(d-bindmv-bind),以及舊宏的名字(destructuring-bindmultiple-value-bind)。第二步,給它們命名並剝離成let表達式的綁定,得到如下的代碼

(let ((new-name 'd-bind)
      (old-name 'destructuring-bind))
  `(defmacro ,new-name (&body body)
     `(,old-name ,@body)))

因爲old-name處於嵌套的反引號中,但是它是由最外層的let定義的,所以應當添上一個前綴的逗號,得到

(let ((new-name 'd-bind)
      (old-name 'destructuring-bind))
  `(defmacro ,new-name (&body body)
     `(,,old-name ,@body)))

最後,因爲old-name綁定的是一個符號,不能被兩次求值(第二次是在defmacro定義的新宏中展開,此時old-name已經被替換爲了destructuring-bind,而它對於新宏而言是一個自由變量,並沒有被綁定),所以需要有一個單引號來阻止第二次的求值——因爲需要的就是符號destructuring-bind本身。所以,最終的代碼爲

(defmacro define-abbreviation (new-name old-name)
  `(defmacro ,new-name (&body body)
     `(,',old-name ,@body)))

試一下就可以確認這個define-abbreviation是能用的(笑

後記

能夠指導編寫宏的、萬能的、機械化的操作方法,我想應該是不存在的

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章