注意:这个问题是指 Julia v1.6。当然,在任何时候,理想情况下,答案也应该回答最新版本的问题。
在 Julia 中似乎存在很多关于宏观卫生的问题和困惑。$name
虽然我阅读了相关的手册页,但在使用插值 ( )quote
和其他引用语法、宏和作用于表达式的函数之间的行为差异等内容时,我仍然很难编写宏esc
。
Julia 提供了哪些工具来查找宏中的错误以及如何有效地使用它们?
这当然是一个广泛的问题,我认为非常值得一个专门的手册页,而不是当前在元编程概述中的事后思考。尽管如此,我认为可以通过考虑和调试一个具体的例子来有效地回答它(即,以一种教我和其他人很多关于主要、一般性问题的方式)。因此,我将讨论一个简单的
玩具示例宏:
(请注意,宏Base.@locals
“构造[s] 名称(作为符号)和定义为调用站点的所有局部变量的值的字典”[来自文档字符串]。)
# Julia 1.5
module MyModule
foo = "MyModule's foo"
macro mac(print_local=true)
println("Dump of argument:{")
dump(print_local)
println("}\n\n")
local_inmacro = "local in the macro"
return quote
println(repeat("-", 30)) # better readability of output
# intention: use variable local to the macro to make a temporary variable in the user's scope
# (can you think of a reason why one might want to do this?)
var_inquote = $local_inmacro * "_modified"
# intention: evaluate `print_local` in user scope
# (THIS CONTAINS AN ERROR ON PURPOSE!
# One should write `if $(esc(print_local))` to achieve intention.)
if $print_local
# intention: get local variables in caller scope
println("Local in caller scope: ", Base.@locals)
else
# intention: local to macro or module AA.
println($foo)
println($local_inmacro)
println(var_inquote)
end
end
end
end # module MyModule
一些代码来测试这个
function testmacro()
foo = "caller's foo"
MyModule.@mac # prints `Dict` containing "caller's foo"
MyModule.@mac true # (Exactly the same)
MyModule.@mac false # prints stuff local to `@mac` and `MyModule`
# If a variable name is passed instead of `true` or `false`,
# it doesn't work. This is because of macro hygiene,
# which renames and rescopes interpolated variables.
# (Intended behaviour is achieved by proper escaping the variable in the macro)
var_false = false
MyModule.@mac var_false # gives `UndefVarError`
end
testmacro()
假装你不明白为什么会发生错误。我们如何知道发生了什么?
调试技术(我知道)包括:
@macroexpand (expr)
: 展开里面的所有宏(expr)
@macroexpand1 (expr)
: 只展开最外层的宏(expr)
,通常只是你正在调试的宏。很有用,例如,如果您正在调试的宏返回带有@warn
内部的表达式,而您不希望看到扩展。macroexpand(m::Module, x; recursive=true)
: 结合以上两者并允许指定“调用者”模块dump(arg)
: 可以在宏内部使用来检查它的参数arg
。eval(expr)
: 评估表达式(几乎不应该在宏体内使用)。
请帮助将有用的东西添加到此列表中。
使用dump
表明print_local
有问题的(即最后一次)宏调用期间的参数是 a Symbol
,准确地说,它的值是:var_false
。
让我们看一下宏返回的表达式。例如,这可以通过将最后一个宏调用 ( MyModule.@mac var_false
)替换为 来完成return (@macroexpand1 MyModule.@mac var_false)
。结果:
quote
#= <CENSORED PATH>.jl:14 =#
Main.MyModule.println(Main.MyModule.repeat("-", 30))
#= <CENSORED PATH>.jl:18 =#
var"#5#var_inquote" = "local in the macro" * "_modified"
#= <CENSORED PATH>.jl:23 =#
if Main.MyModule.var_false
#= <CENSORED PATH>.jl:25 =#
Main.MyModule.println("Local in caller scope: ", #= <CENSORED PATH>.jl:25 =# Base.@locals())
else
#= <CENSORED PATH>.jl:28 =#
Main.MyModule.println("MyModule's foo")
#= <CENSORED PATH>.jl:29 =#
Main.MyModule.println("local in the macro")
#= <CENSORED PATH>.jl:30 =#
Main.MyModule.println(var"#5#var_inquote")
end
end
我们可以手动删除烦人的评论(肯定有内置的方法可以做到这一点?)。
在这个简单的例子中,这里列出的调试工具足以看出问题所在。我们注意到if
宏的返回表达式中的语句将内插符号“重新定义”到宏的父模块:它查看Main.MyModule.var_false
. 我们打算让它Main.var_false
在调用者范围内。
可以通过替换来解决这个if $print_local
问题if $(esc(print_local))
。在这种情况下,宏观卫生将print_local
单独保留变量的内容。esc
对于插入表达式的顺序和位置以及$
插入表达式,我仍然有些困惑。
假设我们搞砸了改写if $esc(print_local)
,因此将esc
函数插入到表达式中,而不是转义任何东西(类似的错误让我很头疼)。这导致返回的表达式(通过获得@macroexpand1
)无法通过执行执行eval
,因为该esc
函数在宏之外很奇怪,返回类似:($(Expr(:escape, <somthing>)))
. 事实上,我通常对Expr
通过获得的 essions何时@macroexpand
真正可执行(与宏调用具有相同的效果)以及如何执行它们(eval
并不总是有效)感到困惑。对此有什么想法吗?