我按照几年前用common lisp的习惯用了一下julia 的macro,结果发现一些不太对劲的地方。不知道是我哪里弄错了还是本身julia 和 common lisp在interpolation上就是有区别。
简述:
在Julia当中使用nest quote时,为了在深入内部的区域使用interpolation,一个$是不够的,你需要双重$$。但问题是,interpolation 出发之后,竟然两个$$没有彻底用掉,还会剩一个下来,然后作为宏的输出代码逃到外面去。使得我们没有办法像common lisp中那样用interpolation来给写宏的宏写宏了。
我们先举个简单例子,我们想写一个宏简单的把后面的代码外面加一层quote起来,即想得到它的表达式。 很自然的,我们的想法是,在双重quote当中去把输入表达式interpolate出来,这样当输出后用掉一层quote之后还剩下一层。
# What I want
# julia @addquote 1+1
# => :(1+1)
#尝试1 - 显然错误
macro addquote(expr)
:(:(expr))
end
#julia> @addquote 1+1
#=> :expr
#出现这个结果很正常,我们需要家$来做interpolate.
#尝试2 - 错误
macro addquote2(expr)
:(:($expr))
end
#julia> @addquote2 1+1
#ERROR: UndefVarError: expr not defined
#出现这个的原因大概也可以理解。这是由于interpolation并没有在macro内运行的时候出发,因此连统expr一起被带到外面去了。 因此外层实际计算时eval了:($expr),而外层没有expr的定义,因此gg。
#尝试3 - 出人意料的错误
macro addquote3(expr)
:(:($$expr))
end
#julia> @addquote3 1+1
#=> 2 (而不是:(1+1))
#我们再加了一层,但是这次让我们非常蛋疼,它把结果给计算出来了。 这是为什么呢?因为实际我发现它输出、并在外层计算的是 :($(1+1)) ! 对!还有一个$没有消掉! 实际上我们dump出来看看:
#修改3, 提供输出表达式的dump
macro addquote3_b(expr)
h = :(:($$expr))
dump(h)
h
end
julia> @addquote3_b 1+1
Expr
head: Symbol quote
args: Array{Any}((1,))
1: Expr
head: Symbol $ <--------------------- look at here!
args: Array{Any}((1,))
1: Expr
head: Symbol call
args: Array{Any}((3,))
1: Symbol +
2: Int64 1
3: Int64 1
2
#我们看到上面确实,生成的表达式当中有一个多余的 $ 表达式。这个东西在common lisp当中是不会有这种困难的。2-2 = 0,而不会产生$这个节点
# 所以我们只能写成下面这个模样
#尝试4-正确的版本
macro addquote4(expr)
Expr(:quote,expr)
end
# julia> @addquote4 1+1
# :(1 + 1)
相比之下 common lisp则美滋滋,你可以看到各种嵌套和interpolation(下面代码来自 《practical common lisp # macro》
(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)))))
只要嵌套深度是对的,就不会出现一个$ 和 expr一起逃到外面去的情况。
我在想,julia这种设计丝毫有些令人吃惊。 实际上嵌套更多层的时候,如果$的数量没有达到嵌套的quote数量,就不会被激活。但一旦激活,却最多只会用掉一个quote。我不是很能理解这种设计的目的或者原因何在。