;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
Macros
lisp 的宏让我们可以定义出新的操作符.
宏是产生lisp代码的函数, 也就是写代码的代码.
这只是一个小的开始, 产生了无数的机会, 也产生了新的危险.
我会用4篇文章来写宏的工作原理, 怎么写怎么测试, 并且学习需要注意哪些宏的风格.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
宏的工作原理
宏可以被调用, 也可以返回值, 宏与函数是一起使用的.
有的时候, 宏重组了函数的定义. 比如, 大家常用的 do 是内置函数, 就是一个宏.
但是这样的类比, 会造成更多的误解.
宏与函数的工作原理是不同的, 我们知道怎么样不同, 为什么这么不同, 我们才能正确地使用它们.
函数返回值, 而宏返回的是表达式, 当eval的时候, 这些表达式来返回值.
来看一个例子.
比如我们想写一个宏 nil!, 将它的参数设置为nil.
我们希望 (nil! x) 与 (setq x nil) 是同样的效果.
我们将nil! 设计为一个宏. 它将第一个表达式, 转换为第二个
(defmacro nil! (var)
(list 'setq var nil))
(defun nil!! (var)
(setq var nil))
(setq v '(1 2 3))
(nil!! v)
(nil! v)
类比为英语, "只要你看到(nil! var), 那么它就是(setq var nil)"
由宏生成的表达式, 而不是宏本身, 将被eval.
宏调用, 是一个list, 这个list的第一个元素是宏的名字.
在toplevel里, lisp会第一时间认出list的第一个元素是一个宏,
然后, 它将
1. 根据宏的内容, 来构建宏生成的表达式.
2. eval表达式的内容.
第1步构建表达式的过程, 被称为宏展开.
(defmacro nil! (var)
(list 'setq var nil))
(nil! v)
lisp 先找 nil! 的定义
nil! 拿到参数, 将参数放进来, 生成新的表达式: setq 参数 nil
最终的宏展开是 (setq v nil)
宏展开之后, 将会进入第二步. evaluation.
toplevel 评估 (setq v nil), 就像你刚刚手打进来一样.
evaluation 并不是在宏展开之后立即进行的.
宏展开是在编译的时候进行的. 而eval是在运行的时候进行的.
展开的时候, 生成表达式, Eval的时候, 生成返回值
正常情况下, 宏展开是非常复杂的.
有时候会出现宏套宏的情况.
此时, 宏会一层层展开到不再有任何宏, 只有表达式的状态.
很多语言都有类似宏的机制.
但是lisp的宏是尤其强大的.
当lisp文件编译的时候, paser读完源代码, 并将它交给编译器.
parser的输出, 是lisp对象组成的 lists.
通过macro, 我们可以掌控parser与编译器之间的格式.
在必要的情况下, 这种掌控是可以极深刻的.
宏展开, 是lisp最强大的功能.
宏就是一个lisp函数, 但是它返回的是lisp语句.
有的宏, 展开后是一个完整的程序.
可以更改编译器的所见, 那么就相当于可以重写编译器.
我们可以将任何的构造加入语言, 只要它可以转换为现有的构造.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
反引号
反引号是一种特别的引号, 专门用来生成表达式模板
反引号最常用的地方, 就是定义宏的时候.
反引号 ` 与引号 ' 功能类似. 当`单独使用的时候, 与'的功能是一模一样的.
`(a b c) ; 与 '(a b c) 一样.
反引号与逗号, 逗号@, 组合的时候, 就变得非常强大了.
反引号制作了一个模板, 而逗号则是在模板里开了一个槽.
`(a b c) ; 类似于 (list 'a 'b 'c)
有逗号的时候, 逗号给lisp说, 关掉保护,
`( a ,b c ,d ) ; 与(list 'a b 'c d) 一样
b的值, 而不是b这个symbol, 被放到了生成的表达式里.
逗号不管在多深的list里, 都是一样作用的, 不受影响
(setq a 1 b 2 c 3)
`(a ,b c)
`(a (,b c))
而且可以出现在嵌套的引号里. 或者没有引号的子列表里.
`( a b ,c (',(+ a b c)) (+ a b) 'c '((,a ,b)))
( a b 3 ('6) (+ a b) 'c '((1 2)))
一个逗号抵消一个反引号的作用. 所以逗呈必须与反引号匹配.
假设, 一个逗号被一个操作符包围, 是指操作符在这个逗号前面, 或者放在已经有逗号在前的列表里.
如:
`(,a ,(b `,c)) ; 最后面的c前面的逗号, 被一个逗号和两个反引号反转.
规则是这样的: 一个被n个逗号包围的逗号, 必须前面有n+1个反引号包围着它.
一个必然的要求是: 逗号不能在反引号之外存在
反引号和逗号可以嵌套. 它们只需要满足上面的规则即可.
下面都是会出错的反例
,x
`(a ,,b c)
`(a ,(b ,c) d)
`(,,'a)
嵌套的反引号看似只会在定义宏的宏里出现.
反引号常用于建立列表
任何由反引号生成的列表, 都可以由list函数和引号来产生.
反引号的优点是它生成的宏特别容易理解
反引号后面, 就是它将成生的表达式.
重定义一个nil!
(defmacro nil! (var)
`(setq ,var nil))
宏定义越长, 越需要用反引号
(defmacro nif (expr pos zero neg)
(list 'case
(list 'truncate (list 'signum expr))
(list 1 pos)
(list 0 zero)
(list -1 neg)))
(defmacro nif (expr pos zero neg)
'(case (truncate (signum ,expr))
(1 ,pos)
(0 ,zero)
(-1 ,neg)))
有了反引号, 直接就可以看出来最后生成的是什么东西. 不然脑子里还要跑一下.
逗@, ,@ 是一个变种的逗号. 它与逗号的工作方式一样, 不同的是, 它不直接取表达式的值,
而且它剥了一下后面的值. 也就是说, 拿掉了后面值里最外层的一层括号.
(setq b '(1 2 3))
`( a ,b c)
`(a ,@b c)
,@还有一些别的限制
1. ,@必须在一个序列里出现, `,@b 肯定是错误的. 因为没有地方来容纳削b后的值.
2. 被削的元素必须是一个列表, 或者是列表里的最后一个.
`(a ,@1 ) ; (a . 1)
`(a ,@1 b) ; 错误
,@常用于不定数量参数的宏定义, 将参数传给函数, 或者传给接收不定额参数的宏.
这种情况常在实现内在block, tagbody, progn 时出现.
这些操作符并不常出现, 它们常常被隐藏在宏里面
任何有body的内置宏里都有隐藏的block.
let cond 都有隐藏的progn, 比如when
(when (eligible )
(do-this)
(do-that)
obj) ; return value
自定义一个when
(defmacro when (test &body body)
`(if (,test)
(progn
,@body )))
这里的&body参数, 和&rest的功能一样, 只是打印出来更好看.
&body取不定数量参数, 放成了一个列表, 所以用,@将它给削皮, 放到progn里.
,@ 的效果也可以不通过反向引号来实现.
`(a ,@b c) 的效果等同于
(cons a (append b (list 'c)))
,@ 是为了让生成的表达式更容易被理解
macro 定义主要是为了产生列表.
macro可以通过 list函数来实现, 但使用反引号可以让写列表模板更容易.
用defmacro 和 反引号可以重写由defun定义的函数
不要不被相似性误导, 你就可以用反引号轻松写出可读的程序.
反引号与宏定义经常一起用, 以至于人们会以为反引号是defmacro的组成部分.
但是你要清楚, 反引号有它自己的作用, 不需要与宏放在一起.
它可以用于任何需要构造list的地方
(defun greet (name)
`(hello ,name))
(greet "zhang yuan")
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
定义简单的宏
想学写程序, 必须边学边真的写.
理论上的理解反而可以将来慢慢来.
我们立即开始学写自己的宏.
它们写起来简单, 但是用到它们的地方很多.
比如, 我们写一个自己的member
member 默认是用eql来做判断的. 如果我们想用eq来判断, 可以显示声明
(member x choices :test #'eq)
老版本的lisp里有一个叫memq的函数, 只用eq来做判断.
我们现在来写一个宏
(defmacro memq (x lst)
`(member ,x ,lst :test #'eq))
来看看每一步思考的过程.
我们先要想出来, 最终是要展开成什么样子.
调用: (memq x choices)
展开: (memeber x choices :test #'eq)
先确定参数名字
(defmacro memq (obj lst)
然后, 我们来写我们准备生成的的东西
将生成的内容用反引号来包围.
然后将展开的内容填进去. 只要不是参数, 就直接填
如果遇到的是宏的参数的值, 那么将这个symbol用参数名替代, 参数名前面要加逗号
现在我们只写了固定参数的宏.
现在我们来试着写一个while, 取一个测试条件, 只要条件为真, 则一直循环下去.
由于是不确定个数的参数, 我们将参数用&body或者&rest来包含.
(defmacro my-while (test &body body)
`(do ()
((not ,test) )
,@body))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
测试宏展开
内置函数 macroexpand 取一个表达式, 并将它宏展开.
(pprint (macroexpand-1 '(my-while (able) (laugh))))
(defmacro mac (expr)
`(pprint (macroexpand-1 ',expr)))
(mac (my-while (> 2 1) (print "hello")))
你可以先将一个宏用macroexpand-1来展开, 然后调用eval来执行它
(setq expr (macroexpand-1 '(memq 'a '(a b c))))
(eval expr)
macroexpand-1 不仅仅是一个调试工具, 也可以用于学习别人的宏是怎么写的.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
将参数列表 destructing
参数的destructiing, 指的是由函数调用来将参数按顺序一一赋值
如果你定义了一个函数
(defun foo (x y z)
(+ x y z ))
然后当你调用它的时候
(foo 1 2 3)
参数: 1 2 3 根据他们的位置分别被赋给了 x y z.
destructing 指的就是将任一个list按顺序进行赋值的操作.
在common lisp里, 有一个叫destructuring-bind的宏
它的参数是一个模式 , 和一组参数, 将参数按模式列表eval绑定进列表的元素上.
还有一个参数是表达式块. 这些表达式块, 可以采用绑定好的列表元素来执行
(destructuring-bind ( x (y) . z) '( a (b) c d )
(list x y z))
destructuring 也可以用于宏的参数列表.
common lisp的 defmacro参数列表可以是任意的列表格式.
当macro被展开的时候, 内部对参数的调用就像destructring-bind是一样的.
内置宏dolist就是采用了这个机制
(dolist (x '(a b c))
(print x))
这里的x就是摘自参数列表的第一个元素, 后面的元素就是要被循环的list
(defmacro our-dolist (var lst &optional result &body body)
`(progn
(mapc #'(lambda (,var) ,body)
,list)
(let ((,var nil))
,result)))
* notice the differences between mapcar and mapc, the return value.
(mapcar #'(lambda (x) (+ 1 x)) '(1 2 3)) ; return list operated
(mapc #'(lambda (x) (+ 1 x)) '(1 2 3)) ; return parameter list
在Common lisp里, 像dolist这样的宏通常将不是body的参数封装进一个列表里.
由于有一个result可选参数, dolist必须将它的第一个参数放到独立的列表里.
即使没有这样的可选项, 将参数独立的放在一个列表里, 也可以让dolist更容易阅读.
我们来定义一个自己的when-bind, 它和when的功能一样,
只是它将一些变量绑定到测试函数的返回值上.
这个宏, 最简单的实现, 是使用嵌套的参数列表
(defmacro when-bind ((var expr) &body body)
`(let ((,var ,expr))
(when ,var
,@body)))
要以调用为
(when-bind (input (get-user-input)) (process input))
有了这个宏, 你就可以不必像下面这样写
(let ((input (get-user-input)))
(when input
(process input)))
谨慎使用参数列表的destructuring, 可以产生更清晰的代码.
至少, 它可以用于像when-bind或者dolist这样的宏, 都是带两个或以上参数,
后面跟着一堆body.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
宏的模型
宏究竟是什么, 很难描述, 科学的描述并不能帮助程序员来记忆它
程序员最好这样理解宏: 它是怎么样被定义出来的.
我们用代码来定义一下宏, 只是简要地看一下它的机制, 真正的实现还是很复杂的.
defmacro自己就是一个macro, 所以我们也可以定义一个自己的.
(defmacro our-expander (name) `(get ,name 'expander)) 这个是宏展开器.
宏展开之后是一堆表达式.
(defmacro our-defmacro (name parms &body body)
(let ((g (gensym))) gensym 生成一个新的symbol
`(progn
(setf (our-expander ',name)
#'(lambda (,g)
(block ,name ; 生成一个叫,name的块
(destructuring-bind ,parms (cdr ,g) ; 这里的cdr是啥意思?
,@body))))
',name)))
defmacro 将它的第一个参数存成了一个函数, 这个函数有两个参数, 第一个参数是宏调用,
第二个参数是调用所处的上下文环境, 这些功能只会被一些很难懂的宏用到
看一下官方的 macro-function 解释:
Determines whether symbol has a function definition as a macro in the specified environment.
If so, the macro expansion function, a function of two arguments, is returned.
我们对这个例子进行手工展开
(our-defmacro our-setq2 (var val)
(list 'setq var val))
what has happened?
name = our-setq2
parms = (var val)
body = (list 'setq var val)
(let ((g (gensym)))
`(progn
(setf ((get our-setq2 'expander) #'(lambda (#g)
(block our-setq2
(destructuring-bind (var val) (cdr #g)
(list 'setq var val)))))
'our-setq)))
play symbol
(setq y (gensym))
(setf (get y 'boss) 'zhangyuan)
(get y 'boss)
(defun our-macroexpand-1 (expr)
(if (and (consp expr) (our-expander (car expr)))
(funcall (our-expander (car expr)) expr)
expr))
(defmacro our-setq (var val)
`(setq ,var ,val))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
宏作为程序
宏定义并不只是一个反引号.
宏是一个将一种表达式转化为另一种表达式的函数.
这个函数用 list 函数来生成结果
但是, 它也可以用来直接调用一个由数百行代码组成的子程序.
我们学了怎么样用反引号来直接将未来要生成的东西写成宏.
但是, 只有最简单的宏可以这样写.
比如, 内置的宏 do 要怎么写呢?
没有办法通过将 do 的参数加上逗号来写出来它.
复杂一些的要生成的表达式可能在宏里根本文治武功不会出现.
写宏更通用的思想是, 想想你准备使用哪种表达式, 也就是说要生成什么样的表达式.
然后写程序将第一种形式转化为第二种形式.
手工将例子展开, 然后看看当第一种形式向第二种形式转化的时候, 究竟发生了什么.
通过这些例子, 你可以发现写宏究竟需要什么样的技能.
下面我们看一个 do 的例子. 看看它的内容是怎么样被展开的.
手工展开是最好的, 学习宏是怎么工作的方式.
比如, 只有亲自动手写了, 你才会发现, 为什么需要 用psetq来更新所有的本地参数
内置的psetq写setq的工作方式类似. 只是它所有的参数, 都会在赋值发生之前被Eval,
如果普通的Setq有超过两个以上的参数, 那么第一个参数的新值, 在别的参数在eval的时候是可见的.
(let ((a 1))
(setq a 2 b a)
(list a b)) ; (2 2)
(let ((a 1))
(psetq a 2 b a)
(list a b)) ; (2 1)
这里 b 是旧的a 的值.
(macroexpand-1 (do ((w 3)
(x 1 (1+ x))
(y 2 (1+ y))
(z))
((> x 10) (princ z) y)
(princ x)
(princ y)))
这里 psetq 非常适合来写 do 宏. 它的几个参数是同时来eval的.
如果我们定义的是do*, 我们就得用Setq了.
(prog ((w 3) (x 1) (y 2) (z nil))
foo
(if (> x 10)
(return (progn (princ z) y)))
(princ x)
(princ y)
(psetq x (1+ x) y (1+ y))
(go foo))
在真实情况下, 我们是没有办法用foo来作为循环标签的.
如果在循环体里, 也有一个叫foo的标签会发生什么呢?
后面我会专门写一篇文章来说明这个问题.
现在, 只需要了解, 我们需要用一个特别由函数gensym返回的匿名symbol来作标签.
为了写出来do, 我们需要观察, 怎么样从上面的第一个形式转化为第二个形式.
想实现这样的转换, 仅仅将宏参数放到反引里的方法是没戏的.
初始化的 prog , 跟着symbol列表,还有他们的初始绑定值, 绑定值从每二个参数里提取分别赋值.
我们专门写一个函数来做这个工作: make-initforms, 它将返回这样的一个列表.
我们也需要为psetq的参数构建一个列表, 然后这个案例要复杂的多,
因为并不是所有的symbol都是同时更新的.
这两个函数写完, 剩余的部分就简单多了.
(defun make-initforms (bindfroms)
(mapcar #'(lambda (b)
(if (consp b)
(list (car b) (cadr b))
(list b nil)))
bindforms))
(defun make-stepforms (bindforms)
(mapcan #'(lambda (b)
(if (and (consp b) (third b))
(list (car b) (third b))
nil))
bindforms))
(defmacro our-do (bindforms (test &rest result) &body body)
(let ((label (gensym)))
`(prog ,(make-initforms bindforms)
,label
(if ,test
(return (progn ,@result)))
,@body
(psetq ,@ (make-stepforms bindforms))
(go ,label))))
上面的程序并不是现实中的do的实现方式.
为了强调展开的时候究竟做了哪些事, 我们将 make-initforms 和 make-stepforms 函数单独拎出来.
将来, 这些代码通常是放在 defmacro 表达式里面的.
通过定义这个宏, 我们可以看到达能可以宏可以做到什么.
宏可以彻底地让lisp构建新表达式, 用来生成展开式的代码, 有可能是一个独立的程序.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
写宏的风格
好的风格对于宏来说不一般.
风格指的是代码用于人读还是用于机器读.
当搞宏的时候, 这两种行为都变得与平常写函数的时候不同了.
当写宏的时候, 牵扯到了两种代码
一种是生成器代码, 一种是被生成的代码.
这两种代码的风格是不一样的.
一般来说, 写程序追求的是清晰和高效.
在写宏的时候, 这两个标准在两种代码里的优先级是不一样的
生成器代码, 需要更清晰, 可以不高效
而被生成的代码, 需要更高效, 可以读不懂
对于编译后的代码, 高效是最关键的. 编译之后, 宏已经被展开了.
如果生成器很高效, 它只会让编译过程变快一点.
但是执行的时候, 并没有什么好处.
宏展开对于编译器来说, 只是一个简单的小任务.
宏展开甚至根本不怎么影响编译时所花的时间.
你有功夫来把宏写得更高效, 那还不如省点力气搞别处的代码.
宏定义相对于函数定义更难于阅读.
如果写生成代码的时候可以清楚一点, 那对人的帮助比让它编译得快点要重要得多.
例如, 我们想写一个 and 宏.
由于 (and a b c) 等于 (if a (if b c)), 我们可以立即写出第一个版本
(defmacro our-and ( &rest args)
(case (length args)
(0 t)
(1 (car args))
(t `(if ,(car args)
(our-and ,@ (cdr args))))))
这个版本我们可以看出写得不怎么样,
它是递归的, 而且每次递归, 都要重新算一次尾巴的长度.
我们对它进行一些改动
(defmacro our-andb ( &rest args)
(if (null args)
t
(labels ((expander (rest)
(if (cdr rest)
`(if ,(car rest)
,(expander (cdr rest)))
(car rest)))))
(expander args)))
setq 会造成各种不便, 因为人很难注意到一个变量是在何时被赋值的.
但是由于宏展开的代码没有人看, 只是给机器看的, 所以宏展开的代码里, 有很多Setq也没有关系.
你会在内置的宏展开时, 看到很多setq的.
宏将用来实现一些通用工具. 这些工具在代码里被广泛使用. 所以效率必须很高.
一个常用的macro比一个长的macro更需要去仔细看它展开后的代码.
看展开后代码效率的另一个原因, 是它非常地不好看.
一个函数没有写好, 你立即就能从定义里看出来它没有写好
但是一个宏没有写好, 你是看不出来的, 所以要习惯于去看宏展开后的代码.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
宏的依赖
当你重定义一个函数的时候, 所以调用这个函数的地方, 都会自动地调用这个新函数.
对于宏则不是这样, 如果一个函数调用了宏.
只有这个函数被重编译之后, 这个宏的展开才会被更新.
如果这个函数编译之后, 你才更新了宏, 那么这个函数的调用, 将依然使用旧宏.
(defmacro mac (x) `(1+ ,x))
(setq fn (compile nil '(lambda (y) (mac y))))
(eval (funcall fn 3))
改一下宏
(defmacro mac (x) `(+ ,x 100))
宏必须在第一次使用之前被定义.
记住以下原则:
1. 在函数调用宏之前, 定义这个宏
2. 重定义宏的时候, 重新编译所有调用它的函数和宏.
有人的建议将所有的宏单独放到一个文件里, 从而保证所有的宏都是先翻译的.
这种做法有点太极端了. 像while这样的通用宏可以单独放.
但是有一些临时用的宏, 可以与调用它的代码放在一起.
如果只是因为宏是宏, 而将它们放在一起, 那样你的代码连续性就被打破了, 代码就太难读了.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
将函数更改成宏
将函数改成宏的第一个问题是, 是否有必要这么做, 你需要问问自己, 是不是可以将函数改成inline函数.
将函数改成宏还是有一些方法的.
你写宏的时候, 可以当自己正在写一个函数, 这样写出来的宏可能不对, 但是你起码有一个雏形可以改.
另外, 你可以观察宏和函数究竟有什么不同.
lisp 程序员往往倾向于将函数写成宏.
将函数写成宏的难度取决于函数的特征.
以下的函数最容易转换成宏
1. 函数体里只有一个表达式
2. 参数列表里, 只有参数名字
3. 除了传进来的参数, 没有产生新的变量的
4. 不包含递归的.
5. 传进来的变量仅被使用过一次
6. 参数的值不被参数列表里位于它之前的参数引用的.
7. 不包含自由变量的.
满足以上要求的函数, 比如内置的second函数
它的定义如下
(defun my-second (lst)
(cadr lst))
它可以很轻易地被改写为宏:
(defmacro m-second (lst)
`(cadr ,lst))
当然, 改写完之后, 也有很多情况不能用.
比如, 它不能被用为 funcall 或者 apply的第一个参数.
也不能在这种环境下被调用, 如果是函数, 可以有新的本地绑定. ; TODO: 这一条我还没有例子可以举出来. 也不是很理解
正常的inline调用, 都是和函数一样的.
当函数不止有一个表达式的时候, 改写的方法会有一点变化.
比如
(defun noisy-second (x)
(princ "here is a function (cadr x) ")
(cadr x))
(defmacro noicy-second (x)
`(progn
(princ "here is a function (cadr x)")
(cadr ,x)))
当函数的参数不仅只有名子, 而是包含的有 &rest 或者 &body, 那么当遇到参数的时候, 不是直接放逗号, 而是放,@, 将列表拿掉.
比如
(defun sum (&rest n)
(apply #'+ n))
(my-sum 1 2 3 4 5)
(defmacro my-sum (&rest args)
`(+ ,@args))
(pprint (macroexpand (my-sum 1 2 3 4 5)))
当第三个条件不满足的时候, 如果函数体里, 声明了新的变量, 那么, 逗号就只能加到参数变量名的前面.
比如函数:
(defun fun-foo (x y z)
(list x (let ((x y)) (list x y z)) ))
(fun-foo 1 2 3)
改写:
(defmacro micro-foo (x y z)
`(list ,x (let ((x ,y)) (list x ,y ,z))))
(micro-foo 1 2 3)
最后的两个x已经不是参数列表里的x了, 而是let里的, 所以不加逗号了.
4,5,6条件违反之后, 有可能是可以转化成宏的, 这个需要具体问题具体分析.
第7条, 宏里可以模拟闭包的, 但这个已经算是极底层的黑魔法, 我就不研究了.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
symbol macro 符宏
普通宏看起来像一个函数调用, 而符宏则看起来像一个符.
符宏只能定义为局部的.
(symbol-macrolet ((hi (progn
(print "hi")
1)))
(+ hi 2))
像定义local 函数一样, 定义了一个符宏.
符宏不带任何的参数, 只是替代了一部分代码.