@宏的展开期和运行期
理解宏的关键在于清楚理解知道那些生成代码的代码(宏)和那些最终构成程序的代码之间的区别。在编译宏的时候,你编译的是将被编译器用来生成代码并随后编译的程序。宏运行的时期被称为宏展开期,这和运行期是不同的,后者是正常代码(包括由宏生成的代码)实际运行的阶段。
@宏的定义
(defmacro name (parameter *)
"Optional documentation string"
body-form*)
接下来我们实现一个简单的宏(从想法到实现)(功能在素数上迭代)
总结起来,编写宏的思路如下三点
1)编写示例的宏调用以及它应当展开成的代码,反之亦然。
2)编写从示例调用的参数中生成手写展开式的代码。
3)确保宏抽象不产生“泄露”。
一:首先我们来编写两个工具函数:
1)判断给定数是否为素数
(defun primep (number)
(when (> number 1)
(loop for fac from 2 to (isqurt number) never (zerop (mod number fac)))))
2)返回大于或等于是参的下一个素数
(defun next-prime (number)
(loop for n from number when (primep n) return n))
二:定义正确的形参来保存不同的实参
(defmacro do-primes ((var start end) &body body)
`(do ((,var (next-prime ,start) (next-prime (+ 1 ,var))))
((>= ,var ,end))
,@body))
比如调用可以(do-primes (p 0 19) (print p))
三:程序漏洞的检查与完善
1:多次求值
比如,它会过多地对end子形式求值。如果用(random 100)这样的表达式在end位置上来调用do-primes。
解决当法可以这样:用一个值对其进行存储
(defmacro do-primes ((var start end) &body body)
`(do ((ending-value ,end)
(,var (next-prime ,start) (next-prime (+ 1 ,var))))
((> ,var ending-value))
,@body))
可是这样会带来两个新漏洞。
一个是:在DO中,变量的初始形式是以变量被定义的顺序来求值的,当宏展开被求值时,传递给end的表达式将在传递给start的表达式之前求值,这与它们出现在宏调用值中的顺序相反。
另一个:问题在于这个名字它可以跟传递给宏的代码或是宏被调用的上下文产生交互。
则最后形式成了:
(defmacro do-primes ((var start end) &body body)
(let ((ending-value-name (gensym)))
`(do ((,var (next-prime ,start) (next-prime (+ 1 ,var)))
(,ending-value-name ,end))
((>= ,var ,ending-value-name))
,@body)))
(macroexpand-1 `(do-primes (p 0 19) (print p)))结果为
(DO ((P (NEXT-PRIME 0) (NEXT-PRIME (+ 1 P))) (#:G3214 19)) ((>= P #:G3214)) (PRINT P)) ;
T
四:然后在将let写成宏,即就是有一个编写宏的宏
(defmacro with-my ((&rest names) &body body)
`(let ,(loop for n in names collect `(,n (gensym)))
,@body))
(defmacro do-primes ((var start end) &body body)
(with-my (ending-value-name)
`(do ((,var (next-prime ,start) (next-prime (+ 1 ,var)))
(,ending-value-name ,end))
((>= ,var ,ending-value-name))
,@body)))