Contents

common lisp macros

macro 在计算机科学中的意义是语言语法上的拓展。lisp 的 macro非常强大,但是需要多实践。

macros 是如何工作的

一个macro 是一段原始的lisp代码,它对另一段假定的lisp代码进行操作,将其转换成更接近于可执行的Lisp代码。有点复杂,举个例子。

1
(setq2 x y e)

当lisp解释器看到这段代码时,会将它视为

1
2
3
(progn
  (setq v1 e)
  (setq v2 e))

实际上,这并不完全正确。macro允许我们通过一段程序,将输入的代码加工成另一个代码

Quote

我们可以这样定义seq2macro:

1
2
(defmacro setq2 (v1 v2 e)
  (list 'progn (list 'setq v1 e) (list 'setq v2 e)))

这段macro接受两个variables 和 一个expression 他会返回一段代码。在lisp中,code是用lists表示的,所以 返回list也就代表一段code.

我们使用了quote符号,一个非常特殊的操作符 每个被quated的对象会被运算成它自己

  • (+ 1 2) evaluates to 3 但是 (quote (+ 1 2)) evalutes to (+ 1 2)
  • (quote (foo bar baz)) evalutes to (foo bar baz)
  • ’ 是quote的所缩写 ‘foo 和 (quote foo 是一样的)

还记得之前说过 ‘用来保护表达式不被求值。‘开头的表达式都会保持原样 所以最后生成的代码是这样的

1
2
3
(progn
  (setq v1 e)
  (setq v2 e))

这时我们就可以使用他了

1
2
3
4
(defparameter v1 1)
(defparameter v2 2)
(setq v1 v2 3)
;; 3

Macroexpand

当你开始写macro的时候,你肯定想知道macro究竟生成了哪些代码。macroexpandkey可以做到这样的事情。

1
2
3
(macroexpand '(setq2 v1 v2 3))
;; (PROGN (SETQ V1 3) (SETQ V2 3))
;; T

更复杂一点的

1
2
3
(macroexpand '(setq2 v1 v2 (+ z 3)))
;; (PROGN (SETQ V1 (+ z 3)) (SETQ V2 (+ z 3)))
;; T

可以看到,e并没有被evaluated 而是保持原样。 我们会需要一个comma(,)符号,用来将被保护的表达式求值

Evaluation Context

在宏定义中,不能再次调用宏,这会导致宏嵌套。如果需要调用某些功能,可以重新定义一个函数,而不是宏。这是因为在执行态时执行的是编译时宏的展开。也就是说,在编译一个函数时,中途遇到了表达式 (setq2 x y (+ z 3),编译器就会将 setq2 宏展开并编译成可执行状态(如机器语言或是字节码)。也就是说,当编译器遇到 setq2 表达式时,他就要切换去执行 setq2 主体内容。而如果在 setq2 里面再嵌套宏时,编译器就会去处理另一个宏,然后就出不来了,导致 setq2 无法继续执行。

在编译时,所有代码都是可以处理的,但是宏嵌套打破了这个规则 错误示例:

1
2
3
4
5
(defmacro setq2 (v1 v2 e)
  (let ((e1 (some-computation e)))
    (list 'progn (list 'setq v1 e1) (list 'setq v2 e1))))

(defmacro some-computation (exp) ...) ;; _Wrong!_

正确示例:

1
2
3
4
5
(defmacro setq2 (v1 v2 e)
  (let ((e1 (some-compatation e)))
    (list 'progn (list 'setq v1 e1) (list 'setq v2 e1))))

(defun some-computation (exp) ...) ;; _Right!_

我们必须要告诉macro 参数是如何传递给幕后函数的。在函数调用中我们很容易做到这样的事情,我们使用lambda-list 语法,例如&optional, &rest, &key,但是在macro中所有的形参都是macro形式的,而不是他们的值。看下面的例子

1
(defmacro foo (x &optional y &key (ctx 'null)) ...)
1
2
3
4
5
_If we call it thus ..._     |_The parameters' values are ..._
-----------------------------|-----------------------------------
`(foo a)`                    | `x=a`, `y=nil`, `cxt=null`
`(foo (+ a 1) (- y 1))`      |`x=(+ a 1)`, `y=(- y 1)`, `cxt=null`
`(foo a b :cxt (zap zip))`   |`x=a`, `y=b`, `cxt=(zap zip)`

你需要清楚的是在宏中变量是不会被求值的,只会保持原样。

看下面的例子

1
2
3
4
5
(defmacro setq-reversible (e1 e2 direction)
  (case direction
    (:normal (list 'setq e1 e2))
    (:backward (list 'setq e2 e1))
    (t (error "Unknown direction: ~a" direction))))

看看他的展开

1
2
3
4
5
6
(macroexpand '(setq-reversible x y :normal))
;;(SETQ X Y)
;;T
(macroexpand '(setq-reversible x y :backward))
;;(SETQ Y X)
;;T

如果你传递了一个错误的参数, 宏展开就会报错

1
(macroexpand '(setq-reversible x y :other-way-around))

我们可以使用backquote 和 comma 来解决宏展开时报错的问题

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
(defmacro setq-reversible (v1 v2 direction)
  (case direction
    (:normal (list 'setq v1 v2))
    (:backward (list 'setq v2 v1))
    (t `(error "Unknown direction: ~a" ,direction))))
    ;; ^^ backquote                    ^^ comma: get the value inside the backquote.

(macroexpand '(SETQ-REVERSIBLE v1 v2 :other-way-around))
;; (ERROR "Unknown direction: ~a" :OTHER-WAY-AROUND)
;; T

使用宏的时候传入错误的方向还是会报错,但是在展开宏的时候,不会报错。

Backquote and comma

backquote(`)字符表明,在他后面的expression,任何不以comma为前缀的表达式都会被quoted,而以comma为前缀的将会被evaluate

1
2
  `(progn (setq ,v1 ,e) (setq ,v2 ,e))
;;^ backquote   ^   ^         ^   ^ commas
1
`(v1 = ,v1) ;; => (V1 = 3)

comma-splice ,@

,@会将(本来应该是列表的)参数展开。将列表的元素插入模板来取代列表

1
2
3
4
5
6
7
8
(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)

Quote-comma ‘,

如果想把表达式的字面打出来,我们需要使用’,

1
2
3
4
5
(defmacro explain-exp (exp)
  `(format t "~s = ~s" ',exp ,exp))

(explain-exp (+ 2 3))
;; (+ 2 3) = 5

Gensym

如果想创建零时变量,我们使用gensym function. 他会返回一个全新的变量,并且不会在别的地方出现

1
2
3
4
5
(defmacro setq2 (v1 v2 e)
  (let ((tempvar (gensym)))
    `(let ((,tempvar ,e))
       (progn (setq ,v1 ,tempvar)
              (setq ,v2 ,tempvar)))))

现在 (setq2 x y (+ x 2)) 会被展开成

1
2
(let ((#:g2003 (+ x 2)))
  (progn (setq x #:g2003) (setq y #:g2003)))