工作原理

宏是 Lisp 的精髓之一,与大部分语言的宏有很大的差别。在一般的语言中,如 C 语言,宏展开就是进行代码替换。而在 Lisp 中,宏是先对表达式进行运算求值展开后,在对最终得到的结果运算求值。也就是说,Lisp 中的宏是边执行边展开,在执行。听上去很怪吧。让我们来看看下面的例子。

假设我们需要拓展 setq,让其可以同时给两个不同变量赋值:

  1. (setq2 x y (+ z 3)

当 z=8 时, x 和 y 被赋值为 11.

显然,setq2 不能是函数。假设 x=50,y=-5,当函数接收到 50,-5和11三个参数时,
函数无法处理。所以,

  1. (setq2 v1 v2 e)

内部工作可以变成如下:

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

然后,setq2 宏可以定义为:

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

再简洁点可以定义为:

  1. (defmacro setq2 (v1 v2 e)
  2. `(progn
  3. (setq ,v1 ,e)
  4. (setq ,v2 ,e)))

定义完后可以检查下返回的结果是否正确:

  1. (defparameter v1 1)
  2. (defparameter v2 2)
  3. v1
  4. ;; 1
  5. v2
  6. ;; 2
  7. (setq2 v1 v2 3)
  8. ;; 3
  9. v1
  10. ;; 3
  11. v3
  12. ;; 3

下面我们开始来详细的介绍该宏的展开情况.
在 Lisp 中,有个 macroexpand 的函数可以查看宏展开后的结果:

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

从上面可以看出,setq2 宏是先执行了主体代码,展开得到我们所期望的代码,然后再对
该结果进行运算求值。这就是所谓的边执行边展开,再执行。

Macros VS functions

上文中的 setq2 宏最开始的定义和函数的定义很接近:

  1. (defun setq2-function (v1 v2 e)
  2. (list 'progn (list 'setq v1 e) (list 'setq v2 e)))

当执行 (setq2-function 'x 'y '(+ z 3)) 时(注:注意函数的参数,都是加了单引号的,传进去的不是变量的值,而是变量名),可以得到相同的结果:

  1. (progn (setq x (+ z 3)) (setq y (+ z 3)))

若是参数不加引号((setq2-function x y (+ z 3))),将会得到错误结果:

  1. (progn (setq 1 11) (setq 2 11))

从得到的结果来看,setq2-function 和 C 语言中的宏替换相似,将得到的参数代入到函数中的表达式中。而 setq2 是将得到的参数求值后再执行表达式。

代码的执行

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

因此:在编译时,所有的代码都是可以处理的。
而宏嵌套就打破了这个原则。

错误示例:

  1. (defmacro setq2 (v1 v2 e)
  2. (let ((e1 (some-computation e)))
  3. (list 'progn (list 'setq v1 e1) (list 'setq v2 e1))))
  4. (defmacro some-computation (exp) ...) ;; _Wrong!_

正确示例:

  1. (defmacro setq2 (v1 v2 e)
  2. (let ((e1 (some-compatation e)))
  3. (list 'progn (list 'setq v1 e1) (list 'setq v2 e1))))
  4. (defun some-computation (exp) ...) ;; _Right!_

反引号和逗号

在 Lisp 中,如果按照宏的正常写法 (list 'prog (list 'setq ...) ...),当宏的功能增加时,宏表达式会变得异常的复杂。因此,Lisp 提供了简写方法:反引号逗号。这两个时配合在一起使用的。当表达式被反引号引用时,在宏展开时不会对表达式进行求值,只会对前面有逗号的值或表达式进行求值。如

  1. `(progn (setq ,v1 ,e) (setq ,v2 ,e))

你可以在这样理解:

  1. `(v1 = ,v1) ;; => (V1 = 3)

在反引用中,如果想要将变量拆分,可以使用 ,@

假设 v=(oh boy),表达式

  1. `(zap ,@v ,v)

等价于 (zap oh boy (oh boy))

如果想要在反引用中直接输出表达式时,可以使用引号加逗号 ',

  1. (defmacro explain-exp (exp)
  2. `(format t "~S = ~S" ',exp ,exp))
  3. (explain-exp (+ 2 3))
  4. ;; (+ 2 3) = 5

你可以自己测试一下:

  1. ;; Defmacro with no quote at all:
  2. (defmacro explain-exp (exp)
  3. (format t "~a = ~a" exp exp))
  4. (explain-exp v1)
  5. ;; V1 = V1
  6. ;; OK, with a backquote and a comma to get the value of exp:
  7. (defmacro explain-exp (exp)
  8. ;; WRONG exmaple
  9. `(format t "~a = ~a" exp ,exp))
  10. (explain-exp v1)
  11. ;; => error: The variable exp is unbound.
  12. ;; We then must use quote-comma:
  13. (defmacro explain-exp (exp)
  14. `(format t "~a = ~a" ',exp ,exp))
  15. (explain-exp (+ 1 2))
  16. ;; (+ 1 2) = 3

建议只有在构建 S-expressions 时才使用反引号。也就是说,当需要构建长度不确定且是由多种元素(如符号、数字和字符串)组成的表达式时才需要用到反引号。

比如说,绝对不要写成这样

  1. (setq sk `(,x @sk))

如果 sk 是一个栈时,可以使用 pop 达到同样的效果 (push x sk),如果 sk 是其他类型时,可以写成 (setq sk (cons x sk))

注:宏的作用是为了拓展Lisp的语法,不是说将所有的函数都改写成宏。

  1. (defun sqone (x)
  2. (let ((y (+ x 1))) (* y y)))
  3. (defmacro sqone (x)
  4. `(let ((y (+ x 1))) (* y y)))

上面这种做法是一种浪费,如果真的有必要将 sqone 展开,可以使用 inline 定义为 (declaim (inline sqone)) (虽然编译器不一定喜欢这个声明)。还有一种情况,如果将 sqone 定义为宏,那就无法通过 (mapcar #'sqone ll) 来调用它了。