5

注意:这个问题是指 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并不总是有效)感到困惑。对此有什么想法吗?

4

0 回答 0