julia 的在nest quote中interpolation似乎和common lisp有些区别


#1

我按照几年前用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。我不是很能理解这种设计的目的或者原因何在。


#2

是的,Julia 需要额外加QuoteNode:

julia> macro addquote3(expr)
           :(:($$(QuoteNode(expr))))
       end
@addquote3 (macro with 1 method)

julia> @addquote3 1+1
:(1 + 1)

julia> macro addquote4(expr)
           Meta.quot(expr)
       end
@addquote4 (macro with 1 method)

julia> @addquote4 1+1
:(1 + 1)

#3

Thanks 楼上。
我回去再琢磨了一下。逐渐意识到关键的问题是在julia当中(或者所有的支持macro interpolation的语言中),interpolation的符号$在外面有多层quote的时候没有办法指定自己到底是跟着那个quote。而$将最终的选择就像是作用域一般的规则:

macro h(a)
        x = :("Inside x ")
        :(($x,:($x)))
end 
x = :("Outside x")
@show @h asdf
# => @h(asdf) = ("Inside x ", "Outside x")

当中,对于右侧比较靠内的$x如果跟的是外面这层1’:()’,那么最终的$x会在macro运行时就替换成:(“Inside x”),并最终输出 (“Inside x”,“Inside x”)。但相反,实际中$x跟的是里面这一层。所以最终输出的是"Outside x"。 这完全就是跟随着作用域的规则。

那么最上面@addquote正常的解决思路也就呼之欲出了,那就是不在多层嵌套的quote当中使用interpolation。 怎么避免它构成多层嵌套呢? 很简单,我们的任务是要对作为data 的expr结构外面再套一层quote。 我们不要直接使用 julia语法当中的quote来添加quote节点。相反我们使用Expr构造器来构造就行了。例如前面的:

macro addquote4(expr)
        Expr(:quote,expr)
end

#or 
macro addquote5(expr)
    :($(Expr(:quote,expr)) 
    # 解释:我们还是使用:(:($x))的思路,只是为了避免内层的quote把$给capture了,我们直接用Expr(:quote,...)来构造。
end