【问题标题】:Julia macro expansion orderJulia 宏扩展顺序
【发布时间】:2016-07-28 20:19:40
【问题描述】:

我想编写一个宏@unpack t,它接受一个对象t,并将其所有字段复制到本地范围内。例如,给定

immutable Foo
    i::Int
    x::Float64
end
foo = Foo(42,pi)

表达式@unpack foo 应扩展为

i = foo.i
x = foo.x

不幸的是,这样的宏不能存在,因为它必须知道传递对象的类型。为了规避这个限制,我引入了一个特定类型的宏@unpackFoo foo,效果相同,但由于我很懒,我希望编译器为我编写@unpackFoo。所以我将类型定义更改为

@unpackable immutable Foo
    i::Int
    x::Float64
end

应该扩展成

immutable Foo
    i::Int
    x::Float64
end
macro unpackFoo(t)
    return esc(quote
        i = $t.i
        x = $t.x
    end)    
end

@unpackable 并不难:

macro unpackable(expr)
    if expr.head != :type
        error("@unpackable must be applied on a type definition")
    end

    name = isa(expr.args[2], Expr) ? expr.args[2].args[1] : expr.args[2]
    fields = Symbol[]
    for bodyexpr in expr.args[3].args
        if isa(bodyexpr,Expr) && bodyexpr.head == :(::)
            push!(fields,bodyexpr.args[1])
        elseif isa(bodyexpr,Symbol)
            push!(fields,bodyexpr)
        end
    end

    return esc(quote
        $expr
        macro $(symbol("unpack"*string(name)))(t)
            return esc(Expr(:block, [:($f = $t.$f) for f in $fields]...))
        end
    end)
end

在 REPL 中,这个定义可以正常工作:

julia> @unpackable immutable Foo
           i::Int
           x::Float64
       end

julia> macroexpand(:(@unpackFoo foo))
quote 
    i = foo.i
    x = foo.x
end

如果我将@unpackFoo 放在与@unpackable 相同的编译单元中,就会出现问题:

julia> @eval begin
       @unpackable immutable Foo
           i::Int
           x::Float64
       end
       foo = Foo(42,pi)
       @unpackFoo foo
       end
ERROR: UndefVarError: @unpackFoo not defined

我认为问题是编译器尝试按如下方式进行

  1. 扩展@unpackable,但不解析它。
  2. 尝试扩展@unpackFoo 失败,因为@unpackable 的扩展尚未被解析。
  3. 如果我们在第 2 步没有失败,编译器现在将解析 @unpackable 的扩展。

这种情况会阻止@unpackable 在源文件中使用。有没有办法告诉编译器交换上面列表中的步骤 2. 和 3.?


这个问题的背景是,我本着https://gist.github.com/jiahao/9240888 的精神,正在研究基于迭代器的迭代求解器实现。像 MinRes 这样的算法在相应的状态对象中需要相当多的变量(目前是 8 个),我不想在每次使用变量时都写 state.variable next() 函数,我也不想手动复制所有这些,因为这会使代码膨胀并且难以维护。最后,这主要是元编程的练习。

【问题讨论】:

  • 我认为您可以更简单地使用生成的函数来做到这一点。但是用例是什么?
  • 我不认为生成的函数会起作用,因为函数引入了一个新的变量范围并且只能在该范围内运行。我将添加一些关于问题背景的评论。
  • 实际上,现在我想到了,在我的用例中,放弃状态类型并改用元组可能更合理,为此我可以通过元组获得@unpack 行为拆包(即i,x = (42,pi))。
  • @gTcV 在 Julia 中解包是通过迭代协议完成的,因此您可以通过实现 startnextdone 来获得类似的行为。见docs.julialang.org/en/release-0.4/manual/interfaces
  • 尝试查看Parameters 包。它有一个@unpack 宏和与您建议的类似的附带机制。

标签: julia


【解决方案1】:

首先,我建议这样写:

immutable Foo
  ...
end

unpackable(Foo)

其中unpackable 是一个函数,它接受类型、构造适当的表达式并evals 它。这有几个优点,例如您可以将其应用于任何类型,而无需在定义时对其进行修复,并且您不必对类型声明进行大量解析(您可以调用 fieldnames(Foo) == [:f, :i] 并使用它)。

其次,虽然我不详细了解您的用例(并且不喜欢一揽子规则),但我会警告说,这种事情是不受欢迎的。它使代码更难阅读,因为它引入了非本地依赖项;突然之间,为了知道x 是局部变量还是全局变量,您必须在完全不同的文件中查找类型的定义。一种更好、更通用的方法是显式解包变量,这可以通过 @destruct 宏在 MacroTools.jl 中获得:

@destruct _.(x, i) = myfoo
# now we can use x and i

(你也可以破坏嵌套的数据结构和可索引的对象,这很好。)

回答您的问题:对于 Julia 运行代码的方式(s/parse/evaluate),您基本上是正确的。整个块被一起解析、扩展和评估,这意味着在您的示例中,您试图在定义 @unpackFoo 之前对其进行扩展。

然而,在加载 .jl 文件时,Julia 一次评估文件中的块,而不是一次评估所有块。

这意味着你可以愉快地编写这样的文件:

macro foo()
  :(println("hi"))
end

@foo()

并运行julia foo.jlinclude("foo.jl"),它会运行良好。您不能在同一个块中拥有宏定义及其使用,就像上面的 begin 块一样。

【讨论】:

    【解决方案2】:

    尝试查看 Mauro (https://github.com/mauro3/Parameters.jl) 的 Parameters 包。它有一个@unpack 宏和与您建议的类似的附带机制。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多