Common LISP自带了case的实现,例如
(setq x 'b)
(case x) 返回nil
(case x
(a (print "it is a"))
((b c) (print "it is b or c"))
(otherwise (print "it is not a b and c")))
case的第一个参数是要判断的值,后面包含多个表达式,每个表达式是一个case分支。
case分支的第一个表达式可以是一个原子,或列表,或otherwise。
用基本规则cond实现case再合适不过了。
(case x)转换成(cond),两者都返回nil
第二个例子转换成下面语句,其中find是自带函数,先用着,后面讲它的实现
(cond ((eq x 'a) (print "it is a"))
((find x (quote (b c))) (print "it is b or c"))
(t (print "it is not a b and c")))
LISP的宏是什么?说白了就是拼装LISP表达式的函数。大家可能在C++或Java中拼过SQL语句,或者拼过HTML网页,LISP的宏也就是这么回事。
实现case宏,就是让它能拼出上面例子中的表达式。因为case的分支数不是固定的,所以要用递归函数拼case的分支语句。
不管怎样,先写一个实现,再慢慢改问题
(defun case.branch (value body)
(cond ((eq nil body) nil)
(t (cons (cond ((eq (caar body) 'otherwise) `(t ,@(cdar body)))
((atom (caar body)) `((eq ,value (quote ,(caar body))) ,@(cdar body)))
(t `((find ,value (quote ,(caar body))) ,@(cdar body))))
(case.branch value (cdr body))))))
(defmacro case. (value &rest body)
(cond ((eq nil body) nil)
(t `(cond ,@(case.branch value body)))))
对于这么复杂的宏,如何验证其正确性,出错时如何修改?函数macroexpand-1可以帮忙,该函数将宏展开,并打印出展开的结果。下面用它来
测试case.是否正确。
(macroexpand-1 '(case. x))
输出:NIL,这是((eq nil body) nil)所起的作用
(macroexpand-1 '(case .x
(a (print 'a))
((b c d) (print 'bcd))))
输出:(COND ((EQ X 'A) (PRINT 'A))
((FIND X '(B C D)) (PRINT 'BCD)))
(macroexpand-1 '(case. (read-char)
(a (print 'a))
(b (print 'b))
(otherwise (print 'other))))
输出:(COND ((EQ (READ-CHAR) 'A) (PRINT 'A))
((EQ (READ-CHAR) 'B) (PRINT 'B))
(T (PRINT 'OTHER)))
最后一个例子出了点问题,本意是读一个字符,却会读很多字符,即每个分支读会读一个字符。
这是实现宏时的典型问题,叫“重复求值”,即输入的参数不是原子,而是一个表达式时,不应该重复对表达式求值。
解决办法时求一次值,将结果赋给一个临时变量,修改宏case.的实现
(defmacro case. (value &rest body)
(let ((_value value))
(cond ((eq nil body) nil)
(t `(cond ,@(case.branch _value body)))))
上面的实现看起来是正确的,实际上犯大错了,没区分开“编译时计算”和“运行时计算”。编译时计算指展开宏时进行的计算,或者说拼语
句时的控制代码。对value的求值应该在运行时。
(defmacro case. (value &rest body)
`(let ((_value ,value))
,(cond ((eq nil body) nil)
(t `(cond ,@(case.branch '_value body))))))
上面实现解决了“重复求值”问题,又引入了“变量捕捉”问题。看下面的代码
(setq _value 'global-value)
(case. 'a (a (print _value)))
期望打印出global-value,实际打印出a,因为let中的局部变量 _value 屏蔽了全局变量 _value
解决此变量捕捉的办法是用gensym生成一个全局唯一的变量名,替代 _value
(defmacro case. (value &rest body)
(let ((_value (gensym)))
`(let ((,_value ,value))
,(cond ((eq nil body) nil)
(t `(cond ,@(case.branch _value body)))))))
测试一下
(macroexpand-1 '(case. 'a (a (print _value))))
输出:(LET ((#:G3555 'A)) (COND ((EQ #:G3555 'A) (PRINT _VALUE))))