工作原理
宏是 Lisp 的精髓之一,与大部分语言的宏有很大的差别。在一般的语言中,如 C 语言,宏展开就是进行代码替换。而在 Lisp 中,宏是先对表达式进行运算求值展开后,在对最终得到的结果运算求值。也就是说,Lisp 中的宏是边执行边展开,在执行。听上去很怪吧。让我们来看看下面的例子。
假设我们需要拓展 setq
,让其可以同时给两个不同变量赋值:
(setq2 x y (+ z 3)
当 z=8 时, x 和 y 被赋值为 11.
显然,setq2
不能是函数。假设 x=50,y=-5,当函数接收到 50,-5和11三个参数时,
函数无法处理。所以,
(setq2 v1 v2 e)
内部工作可以变成如下:
(progn
(setq v1 e)
(setq v2 e))
然后,setq2
宏可以定义为:
(defmacro setq2 (v1 v2 e)
(list 'progn (list 'setq v1 e) (list 'setq v2 e)))
再简洁点可以定义为:
(defmacro setq2 (v1 v2 e)
`(progn
(setq ,v1 ,e)
(setq ,v2 ,e)))
定义完后可以检查下返回的结果是否正确:
(defparameter v1 1)
(defparameter v2 2)
v1
;; 1
v2
;; 2
(setq2 v1 v2 3)
;; 3
v1
;; 3
v3
;; 3
下面我们开始来详细的介绍该宏的展开情况.
在 Lisp 中,有个 macroexpand
的函数可以查看宏展开后的结果:
(macroexpand '(setq2 v1 v2 3))
;; (PROGN (SETQ V1 3) (SETQ V2 3))
;; T
从上面可以看出,setq2
宏是先执行了主体代码,展开得到我们所期望的代码,然后再对
该结果进行运算求值。这就是所谓的边执行边展开,再执行。
Macros VS functions
上文中的 setq2
宏最开始的定义和函数的定义很接近:
(defun setq2-function (v1 v2 e)
(list 'progn (list 'setq v1 e) (list 'setq v2 e)))
当执行 (setq2-function 'x 'y '(+ z 3))
时(注:注意函数的参数,都是加了单引号的,传进去的不是变量的值,而是变量名),可以得到相同的结果:
(progn (setq x (+ z 3)) (setq y (+ z 3)))
若是参数不加引号((setq2-function x y (+ z 3))
),将会得到错误结果:
(progn (setq 1 11) (setq 2 11))
从得到的结果来看,setq2-function
和 C 语言中的宏替换相似,将得到的参数代入到函数中的表达式中。而 setq2
是将得到的参数求值后再执行表达式。
代码的执行
在宏定义中,不能再次调用宏,这会导致宏嵌套。如果需要调用某些功能,可以重新定义一个函数,而不是宏。这是因为在执行态时执行的是编译时宏的展开。也就是说,在编译一个函数时,中途遇到了表达式 (setq2 x y (+ z 3)
,编译器就会将 setq2
宏展开并编译成可执行状态(如机器语言或是字节码)。也就是说,当编译器遇到 setq2
表达式时,他就要切换去执行 setq2
主体内容。而如果在 setq2
里面再嵌套宏时,编译器就会去处理另一个宏,然后就出不来了,导致 setq2
无法继续执行。
因此:在编译时,所有的代码都是可以处理的。
而宏嵌套就打破了这个原则。
错误示例:
(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!_
正确示例:
(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!_
反引号和逗号
在 Lisp 中,如果按照宏的正常写法 (list 'prog (list 'setq ...) ...)
,当宏的功能增加时,宏表达式会变得异常的复杂。因此,Lisp 提供了简写方法:反引号 和 逗号。这两个时配合在一起使用的。当表达式被反引号引用时,在宏展开时不会对表达式进行求值,只会对前面有逗号的值或表达式进行求值。如
`(progn (setq ,v1 ,e) (setq ,v2 ,e))
你可以在这样理解:
`(v1 = ,v1) ;; => (V1 = 3)
在反引用中,如果想要将变量拆分,可以使用 ,@
。
假设 v=(oh boy),表达式
`(zap ,@v ,v)
等价于 (zap oh boy (oh boy))
如果想要在反引用中直接输出表达式时,可以使用引号加逗号 ',
。
(defmacro explain-exp (exp)
`(format t "~S = ~S" ',exp ,exp))
(explain-exp (+ 2 3))
;; (+ 2 3) = 5
你可以自己测试一下:
;; Defmacro with no quote at all:
(defmacro explain-exp (exp)
(format t "~a = ~a" exp exp))
(explain-exp v1)
;; V1 = V1
;; OK, with a backquote and a comma to get the value of exp:
(defmacro explain-exp (exp)
;; WRONG exmaple
`(format t "~a = ~a" exp ,exp))
(explain-exp v1)
;; => error: The variable exp is unbound.
;; We then must use quote-comma:
(defmacro explain-exp (exp)
`(format t "~a = ~a" ',exp ,exp))
(explain-exp (+ 1 2))
;; (+ 1 2) = 3
建议只有在构建 S-expressions
时才使用反引号。也就是说,当需要构建长度不确定且是由多种元素(如符号、数字和字符串)组成的表达式时才需要用到反引号。
比如说,绝对不要写成这样
(setq sk `(,x @sk))
如果 sk
是一个栈时,可以使用 pop
达到同样的效果 (push x sk)
,如果 sk
是其他类型时,可以写成 (setq sk (cons x sk))
。
注:宏的作用是为了拓展Lisp的语法,不是说将所有的函数都改写成宏。
(defun sqone (x)
(let ((y (+ x 1))) (* y y)))
(defmacro sqone (x)
`(let ((y (+ x 1))) (* y y)))
上面这种做法是一种浪费,如果真的有必要将 sqone
展开,可以使用 inline
定义为 (declaim (inline sqone))
(虽然编译器不一定喜欢这个声明)。还有一种情况,如果将 sqone
定义为宏,那就无法通过 (mapcar #'sqone ll)
来调用它了。