Julia宏扩展顺序

3
我希望编写一个宏 @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. 如果我们在第二步没有失败,那么编译器现在将解析@unpackable的扩展。
这种情况阻止了@unpackable在源文件中的使用。有没有办法告诉编译器在上述列表中交换步骤2和3?
这个问题的背景是,我正在基于迭代求解器的实现中工作,这类求解器的实现方式类似于https://gist.github.com/jiahao/9240888。像MinRes这样的算法需要相应状态对象中的许多变量(目前为8个),我既不想每次在next()函数中使用变量时都写state.variable,也不想手动复制所有变量,因为这会使代码膨胀并且难以维护。最终,这主要是元编程的练习。

1
我不认为生成的函数会起作用,因为函数引入了一个新的变量作用域,并且只能在该作用域内操作。我将添加一些关于问题背景的注释。 - gTcV
其实,现在我想起来了,在我的使用情况下,放弃状态类型并改用元组可能更合理,这样我可以通过解包元组(即 i,x =(42,pi))获得 @unpack 行为。 - gTcV
1
在Julia中,解包是通过迭代协议完成的,因此您可以通过实现“start”,“next”和“done”来获得类似的行为。请参见http://docs.julialang.org/en/release-0.4/manual/interfaces/。 - Fengyang Wang
如何解包自定义类型的示例请参见 http://stackoverflow.com/questions/38289726/julia-custom-type-assignment/38290263?noredirect=1#comment63999270_38290263。 - Daniel Høegh
1
尝试查看Parameters包。它有一个@unpack宏和相应的机制,类似于您所需的建议。 - Dan Getz
显示剩余2条评论
2个回答

1

1
首先,我建议将其写成以下形式:
immutable Foo
  ...
end

unpackable(Foo)

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

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

@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.jl 或者 include("foo.jl") 即可正常运行。只是不能在同一块中定义宏并使用它,就像你上面的 begin 块一样。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接