QuoteNode Meta.quot 和 Expr(引用)
使用 Julia 函数引用某些内容有三种方法:
julia> QuoteNode(:x)
:(:x)
julia> Meta.quot(:x)
:(:x)
julia> Expr(:quote, :x)
:(:x)
引用是什么意思,有什么好处?引用允许我们保护表达式不被 Julia 解释为特殊形式。一个常见的用例是当我们生成应该包含对符号求值的东西的表达式时。 (例如,此宏需要返回一个计算符号的表达式。)它不能简单地返回符号:
julia> macro mysym(); :x; end
@mysym (macro with 1 method)
julia> @mysym
ERROR: UndefVarError: x not defined
julia> macroexpand(:(@mysym))
:x
这里发生了什么? @mysym
扩展为:x
,其表达式被解释为变量 x
。但是还没有分配给 x
,所以我们得到了一个 x not defined
错误。
为了解决这个问题,我们必须引用宏的结果:
julia> macro mysym2(); Meta.quot(:x); end
@mysym2 (macro with 1 method)
julia> @mysym2
:x
julia> macroexpand(:(@mysym2))
:(:x)
在这里,我们使用 Meta.quot
函数将符号转换为带引号的符号,这是我们想要的结果。
Meta.quot
和 QuoteNode
有什么区别,我应该使用哪个?几乎在所有情况下,差异并不重要。有时使用 QuoteNode
而不是 Meta.quot
可能会更安全一些。然而,探索差异可以提供有关 Julia 表达式和宏如何工作的信息。
解释了 Meta.quot
和 QuoteNode
之间的区别
这是一条经验法则:
- 如果你需要或想要支持插值,请使用
Meta.quot
; - 如果你不能或不想允许插值,请使用
QuoteNode
。
简而言之,区别在于 Meta.quot
允许在引用的内容中进行插值,而 QuoteNode
保护其参数不受任何插值的影响。要理解插值,重要的是要提到 $
表达式。朱莉娅有一种称为 $
表达的表达方式。这些表达式允许转义。例如,请考虑以下表达式:
julia> ex = :( x = 1; :($x + $x) )
quote
x = 1
$(Expr(:quote, :($(Expr(:$, :x)) + $(Expr(:$, :x)))))
end
在评估时,该表达式将评估 1
并将其分配给 x
,然后构造 _ + _
形式的表达式,其中 _
将被 x
的值替换。因此,其结果应该是表达 1 + 1
(尚未评估,因此与值 2
不同)。的确,情况就是这样:
julia> eval(ex)
:(1 + 1)
现在让我们说我们正在编写一个宏来构建这些表达式。我们的宏将采取一个论点,它将取代上面的 ex
中的 1
。当然,这个论点可以是任何表达。这是我们想要的东西:
julia> macro makeex(arg)
quote
:( x = $(esc($arg)); :($x + $x) )
end
end
@makeex (macro with 1 method)
julia> @makeex 1
quote
x = $(Expr(:escape, 1))
$(Expr(:quote, :($(Expr(:$, :x)) + $(Expr(:$, :x)))))
end
julia> @makeex 1 + 1
quote
x = $(Expr(:escape, 2))
$(Expr(:quote, :($(Expr(:$, :x)) + $(Expr(:$, :x)))))
end
第二种情况是不正确的,因为我们应该保持这种情况不被评估。我们通过引用 Meta.quot
的参数来解决这个问题:
julia> macro makeex2(arg)
quote
:( x = $$(Meta.quot(arg)); :($x + $x) )
end
end
@makeex2 (macro with 1 method)
julia> @makeex2 1 + 1
quote
x = 1 + 1
$(Expr(:quote, :($(Expr(:$, :x)) + $(Expr(:$, :x)))))
end
宏观卫生不适用于引号的内容,因此在这种情况下不需要转义(实际上不合法)。
如前所述,Meta.quot
允许插值。所以让我们尝试一下:
julia> @makeex2 1 + $(sin(1))
quote
x = 1 + 0.8414709848078965
$(Expr(:quote, :($(Expr(:$, :x)) + $(Expr(:$, :x)))))
end
julia> let q = 0.5
@makeex2 1 + $q
end
quote
x = 1 + 0.5
$(Expr(:quote, :($(Expr(:$, :x)) + $(Expr(:$, :x)))))
end
从第一个例子中,我们看到插值允许我们内联 sin(1)
,而不是使表达式成为文字 sin(1)
。第二个示例显示此插值是在宏调用范围内完成的,而不是宏自己的范围。那是因为我们的宏实际上没有评估任何代码; 它正在做的就是生成代码。当宏生成的表达式实际运行时,代码的评估(进入表达式)就完成了。
如果我们使用了 QuoteNode
怎么办?正如你可能猜到的那样,由于 QuoteNode
可以防止插值发生,这意味着它不起作用。
julia> macro makeex3(arg)
quote
:( x = $$(QuoteNode(arg)); :($x + $x) )
end
end
@makeex3 (macro with 1 method)
julia> @makeex3 1 + $(sin(1))
quote
x = 1 + $(Expr(:$, :(sin(1))))
$(Expr(:quote, :($(Expr(:$, :x)) + $(Expr(:$, :x)))))
end
julia> let q = 0.5
@makeex3 1 + $q
end
quote
x = 1 + $(Expr(:$, :q))
$(Expr(:quote, :($(Expr(:$, :x)) + $(Expr(:$, :x)))))
end
julia> eval(@makeex3 $(sin(1)))
ERROR: unsupported or misplaced expression $
in eval(::Module, ::Any) at ./boot.jl:234
in eval(::Any) at ./boot.jl:233
在这个例子中,我们可能会同意 Meta.quot
提供更大的灵活性,因为它允许插值。那么为什么我们会考虑使用 QuoteNode
?在某些情况下,我们可能实际上不希望插值,并且实际上想要文字 $
表达式。什么时候可取?让我们考虑一下 @makeex
的概括,我们可以通过其他参数来确定+
标志的左侧和右侧:
julia> macro makeex4(expr, left, right)
quote
quote
$$(Meta.quot(expr))
:($$$(Meta.quot(left)) + $$$(Meta.quot(right)))
end
end
end
@makeex4 (macro with 1 method)
julia> @makeex4 x=1 x x
quote # REPL[110], line 4:
x = 1 # REPL[110], line 5:
$(Expr(:quote, :($(Expr(:$, :x)) + $(Expr(:$, :x)))))
end
julia> eval(ans)
:(1 + 1)
我们实现 @makeex4
的一个限制是我们不能直接将表达式用作表达式的左侧和右侧,因为它们被插值。换句话说,可以对表达式进行插值评估,但我们可能希望保留它们。 (由于这里有很多级别的引用和评估,让我们澄清一下:我们的宏生成的代码构造了一个表达式,当计算得到另一个表达式时 .Phew!)
julia> @makeex4 x=1 x/2 x
quote # REPL[110], line 4:
x = 1 # REPL[110], line 5:
$(Expr(:quote, :($(Expr(:$, :(x / 2))) + $(Expr(:$, :x)))))
end
julia> eval(ans)
:(0.5 + 1)
我们应该允许用户指定何时进行插值,何时不应该进行插值。从理论上讲,这是一个简单的解决方案:我们可以在我们的应用程序中删除其中一个 $
标志,并让用户自己贡献。这意味着我们插入用户输入的表达式的引用版本(我们已经引用并插入一次)。这导致了以下代码,由于多个嵌套级别的引用和取消引用,这些代码起初可能有点混乱。尝试阅读并理解每个转义的用途。
julia> macro makeex5(expr, left, right)
quote
quote
$$(Meta.quot(expr))
:($$(Meta.quot($(Meta.quot(left)))) + $$(Meta.quot($(Meta.quot(right)))))
end
end
end
@makeex5 (macro with 1 method)
julia> @makeex5 x=1 1/2 1/4
quote # REPL[121], line 4:
x = 1 # REPL[121], line 5:
$(Expr(:quote, :($(Expr(:$, :($(Expr(:quote, :(1 / 2)))))) + $(Expr(:$, :($(Expr(:quote, :(1 / 4)))))))))
end
julia> eval(ans)
:(1 / 2 + 1 / 4)
julia> @makeex5 y=1 $y $y
ERROR: UndefVarError: y not defined
事情开始很好,但出了点问题。宏生成的代码试图在宏调用范围内插入 y
的副本; 但是在宏调用范围内没有 y
的副本。我们的错误是允许使用宏中的第二个和第三个参数进行插值。要解决此错误,我们必须使用 QuoteNode
。
julia> macro makeex6(expr, left, right)
quote
quote
$$(Meta.quot(expr))
:($$(Meta.quot($(QuoteNode(left)))) + $$(Meta.quot($(QuoteNode(right)))))
end
end
end
@makeex6 (macro with 1 method)
julia> @makeex6 y=1 1/2 1/4
quote # REPL[129], line 4:
y = 1 # REPL[129], line 5:
$(Expr(:quote, :($(Expr(:$, :($(Expr(:quote, :(1 / 2)))))) + $(Expr(:$, :($(Expr(:quote, :(1 / 4)))))))))
end
julia> eval(ans)
:(1 / 2 + 1 / 4)
julia> @makeex6 y=1 $y $y
quote # REPL[129], line 4:
y = 1 # REPL[129], line 5:
$(Expr(:quote, :($(Expr(:$, :($(Expr(:quote, :($(Expr(:$, :y)))))))) + $(Expr(:$, :($(Expr(:quote, :($(Expr(:$, :y)))))))))))
end
julia> eval(ans)
:(1 + 1)
julia> @makeex6 y=1 1+$y $y
quote # REPL[129], line 4:
y = 1 # REPL[129], line 5:
$(Expr(:quote, :($(Expr(:$, :($(Expr(:quote, :(1 + $(Expr(:$, :y)))))))) + $(Expr(:$, :($(Expr(:quote, :($(Expr(:$, :y)))))))))))
end
julia> @makeex6 y=1 $y/2 $y
quote # REPL[129], line 4:
y = 1 # REPL[129], line 5:
$(Expr(:quote, :($(Expr(:$, :($(Expr(:quote, :($(Expr(:$, :y)) / 2)))))) + $(Expr(:$, :($(Expr(:quote, :($(Expr(:$, :y)))))))))))
end
julia> eval(ans)
:(1 / 2 + 1)
通过使用 QuoteNode
,我们保护我们的参数不受插值。由于 QuoteNode
只具有额外保护的效果,因此除非你需要插值,否则使用 QuoteNode
永远不会有害。但是,了解这种差异可以了解 Meta.quot
可能是更好的选择的地点和原因。
这个漫长的练习是一个明显过于复杂的例子,无法在任何合理的应用中出现。因此,我们已经证明了前面提到的以下经验法则:
- 如果你需要或想要支持插值,请使用
Meta.quot
; - 如果你不能或不想允许插值,请使用
QuoteNode
。
那么 Expr(:引用)呢?
Expr(:quote, x)
相当于 Meta.quot(x)
。然而,后者更具惯用性并且是优选的。对于大量使用元编程的代码,经常使用 using Base.Meta
线,这使得 Meta.quot
可以简称为 quot
。