定义宏时,必须留意参数在展开代码执行时会被求值多少次。下面这个用于简化循环的宏示例,就展示了这类问题。该宏可以让我们写出类似 for 循环的结构。
(defmacro for (var from init to final do &rest body)
"Execute a simple \"for\" loop.
For example, (for i from 1 to 10 do (print i))."
(list 'let (list (list var init))
(cons 'while
(cons (list '<= var final)
(append body (list (list 'inc var)))))))
(for i from 1 to 3 do (setq square (* i i)) (princ (format "\n%d %d" i square))) →
(let ((i 1))
(while (<= i 3)
(setq square (* i i))
(princ (format "\n%d %d" i square))
(inc i)))
⊣1 1
⊣2 4
⊣3 9
⇒ nil
该宏中的 from、to 和 do 参数仅作为语法糖(syntactic sugar) 存在 —— 它们会被完全忽略。设计思路是让你在宏调用的这些位置写入这类 “无实际作用的辅助词”(例如 from、to、do),仅用于提升代码可读性。
以下是使用反引号简化后的等效定义:
(defmacro for (var from init to final do &rest body)
"Execute a simple \"for\" loop.
For example, (for i from 1 to 10 do (print i))."
`(let ((,var ,init))
(while (<= ,var ,final)
,@body
(inc ,var))))
该定义的两种写法(使用反引号和不使用反引号)都存在一个缺陷:final 会在每次循环迭代时都被求值。若 final 是常量,这不会有问题;但如果它是更复杂的表达式(例如 (long-complex-calculation x)),则会显著降低执行速度;若 final 带有副作用,多次执行它很可能导致逻辑错误。
一个设计良好的宏定义会主动规避这类问题:生成的展开式会确保参数表达式仅被求值一次(除非重复求值是该宏的设计意图)。以下是 for 宏的正确展开版本:
(let ((i 1)
(max 3))
(while (<= i max)
(setq square (* i i))
(princ (format "%d %d" i square))
(inc i)))
以下是能生成该展开式的宏定义:
(defmacro for (var from init to final do &rest body)
"Execute a simple for loop: (for i from 1 to 10 do (print i))."
`(let ((,var ,init)
(max ,final))
(while (<= ,var max)
,@body
(inc ,var))))
遗憾的是,这一修复方案引入了另一个问题,具体将在下一节说明。