1 practical common lisp
1.1 once-only macro explanation
: (defmacro once-only ((&rest names) &body body) : (let ((gensyms (loop for n in names collect (gensym)))) : `(let (,@(loop for g in gensyms collect `(,g (gensym)))) : `(let (,,@(loop for g in gensyms for n in names collect ``(,,g ,,n))) : ,(let (,@(loop for n in names for g in gensyms collect `(,n ,g))) : ,@body))))) : (defmacro t5 ((a) &body body) : `(let ((x1 ,a)) : (format t "x1: ~a~%" x1) : `(let ((x2 (+ ,x1 ,,a))) : (format t "x2: ~a~%" x2) : ,(let ((x3 (+ x1 5))) : (format t "X3: ~a~%" x3))))) : (defmacro t55 (x) : (t5 (x)))
先看t5的执行情况,可以看出,没有`范围的代码,是在macroexpan-1后出结果
: CL-USER> (macroexpand-1 '(t5 (100))) : (LET ((X1 100)) : (FORMAT T "x1: ~a~%" X1) : (LIST* 'LET (LIST* (LIST (LIST* 'X2 (LIST (LIST* '+ (LIST* X1 (LIST 100)))))) : (LIST* '(FORMAT T "x2: ~a~%" X2) : (LIST (LET ((X3 (+ X1 5))) : (FORMAT T "X3: ~a~%" X3)))))))
可以看出,有一个`的范围内,在执行(eval (macroexpand-1后出结果
: CL-USER> (eval (macroexpand-1 '(t5 (100)))) : x1: 100 : X3: 105 : (LET ((X2 (+ 100 100))) (FORMAT T "x2: ~a~%" X2) NIL)
可以看出,在(eval和(eval (macroexpand-1的结果一样的
: CL-USER> (eval '(t5 (100))) : x1: 100 : X3: 105 : (LET ((X2 (+ 100 100))) (FORMAT T "x2: ~a~%" X2) NIL)
总结:对于macro代码的阅读,可以按照“`”来分级数,一个"`"表示加一级,而“,”表示减一级。每一级需要(eval一次。
可以看出,有两个``范围的内的代码,在执行(eval (eval (macroexpand-1后出结果
: CL-USER> (eval (eval (macroexpand-1 '(t5 (100))))) : x1: 100 : X3: 105 : x2: 200 : NIL
defmcro嵌套的级数对应于eval的次数。
: CL-USER> (t55 10) : x1: 10 : X3: 15 : x2: 20 : NIL
once-only的分解
: (defmacro once-only ((&rest names) &body body) : (let ((gensyms (loop for n in names collect (gensym)))) : `(let (,@(loop for g in gensyms collect `(,g (gensym)))) : `(let (,,@(loop for g in gensyms for n in names collect ``(,,g ,,n))) : ,(let (,@(loop for n in names for g in gensyms collect `(,n ,g))) : ,@body)))))
假设代码为 (once-only (a) (list 'list a))
: (let ((gensyms (loop for n in names collect (gensym))))
这一行在macroexpand-1时已经当做代码执行了,并产生了gensyms 假设为 #:G386
: `(let (,@(loop for g in gensyms collect `(,g (gensym))))
这一行在macroexpan后,产生代码(let ((#:G386 (gensym)))) ;; ,g 表示g要替换为g变量的值,也就是gensyms中的元素
: `(let (,,@(loop for g in gensyms for n in names collect ``(,,g ,,n)))
这一行是要第二次eval后才能产生代码的,,,g替换为了,g的值,即#:G386, ,,n替换为了入参的变量名
: ,(let (,@(loop for n in names for g in gensyms collect `(,n ,g))) : ,@body)
这一段用了一个","在(let前面,把两级的"`"变为了一级,所以第一次eval后就产生了代码(list (let ((a #:G386)) (list 'list a)) 以及body中的",n"和",g"等变量都会替换其值,产生代码
所以,macroexpand后的代码为:
: (LET ((#:G386 (GENSYM))) : (LIST* 'LET (LIST* (LIST (LIST* #:G386 (LIST A))) : (LIST (LET ((A #:G386)) : (LIST 'LIST A))))))
第一次eval时,产生了第二轮变量名 假设为 #:G391
: (let ((,g (gensym)))) #:G386 = #:G391 : `(let (,,@(loop for g in gensyms for n in names collect ``(,,g ,,n)))
产生代码,(let ((#:G391 nval))) 这里,,g为第二轮产生的 (gensym), nval为names传进来的值
,n = ,g = ,,g的名字
: (let ((,n ,g)))
所以第一次eval后的代码为:
: (LET ((#:G391 11)) (LIST #:G391))
第二次eval后即为结果:(11)
书中说编写macro的macro,这个需要两次eval是对应的,因为每次macroexpand-1时,对于没有"`"的代码是会直接执行的。
总结出来macro解析的规律为:
- 每次调用macro时,相当于 (eval (macroexpand-1 (some-macro)))
- 每次调用macroexpand-1时,对于未"`"的代码是直接运行,并对"`"中的代码产生替换;对于"`"一级的代码,直接产生 (eval)后可以出结果的代码,对于"``"中的代码,产生两次(eval (eval))后能够产生代码的代码。对于有两次eval才能产生代码的macro,一般应用于编写macro的macro,因为每次macroexpand-1都会eval一次没有"`"的代码 (这种macro不能包含在"`"里面) 。对于,var的变量,会把,var替换为新的变量名。例如: (defmacro name1 (x)`(list ,x)) (macroexpand-1 '(name1 a)) 会把,x换位a。
- 调用eval时,会把expand后的代码做一次运行。对于变量,不会再有,var,也就不再替换变量名,只会把变量换为变量的值。