【问题标题】:How to unpack a Julia struct entirely into local variables?如何将 Julia 结构完全解压缩到局部变量中?
【发布时间】:2021-04-17 13:35:34
【问题描述】:

我创建了一个包含大量参数的复杂计算模型。由于我需要运行许多场景,我决定将所有这些输入参数包装成一个巨大的struct

using Parameters
@with_kw struct MyModel
    a::Int = 5
    b::Float64 = 5.5
    c::Matrix{Float64} = rand(3,4)
    # 40 other parameters go here
end

我有一个对象m 为例:

m = MyModel(a=15)

现在在编写数学代码时,我不想在每个符号前面写m.。因此,我需要将结构字段转换为局部变量。一种方法是使用@unpack 宏:

@unpack a, b, c = m

对于我想在各种函数中解包的大型结构,这很不方便(请注意,我的结构有大约 40 个字段)。如何在不花费时间和将所有这些参数弄乱我的代码的情况下解压缩结构?

【问题讨论】:

    标签: struct julia metaprogramming


    【解决方案1】:

    Parameters.jl 中的宏 @with_kw 为此定义了一个宏:

    julia> using Parameters
    
    julia> @with_kw struct MyModel  # exactly as in the question
               a::Int = 5
               b::Float64 = 5.5
               c::Matrix{Float64} = rand(3,4)
               # 40 other parameters go here
           end
    MyModel
    
    julia> @macroexpand @unpack_MyModel x
    quote
        a = x.a
        b = x.b
        c = x.c
    end
    

    因此,当您知道m isa MyModel 时,写@unpack_MyModel m 等同于写@unpack a, b, c = m

    【讨论】:

    • 谢谢!与自己编写宏相比,这确实节省了时间!
    • 我想这是想要超过 Base.@kwdef struct MyModel 的包的原因之一,它不会这样做。
    • 当您在控制台中显示结构时,我最喜欢的原因是结构更好的可视化 :-) 无论如何?@with_kw 并没有说明这个漂亮的宏得到了实现。
    【解决方案2】:

    另一个选项是StaticModules.jl。这是我从该软件包的 README 中复制并粘贴的示例:

    julia> struct Bar
               a
               b
           end
    
    julia> @with Bar(1, 2) begin
               a^2, b^2
           end
    (1, 4)
    

    【讨论】:

    • 通常我的计算代码非常大,并且经常包含其他宏调用(例如,因为使用了 JuMP)。我想知道@with 对于复杂代码块的鲁棒性如何。
    • 好问题。我自己并没有真正使用过 StaticModules.jl。 :)
    【解决方案3】:

    对于自定义案例,您可以为您的类型制作一个专用宏,对于其他案例,请参阅上面的答案:

    macro unpack_MyModel(q)
        code =  Expr(:block, [ :($field = $q.$field) for field in fieldnames(MyModel) ]...)
        esc(code)
    end
    

    这只是插入以下代码:

    julia> @macroexpand @unpack_MyModel(m)
    quote
        a = m.a
        b = m.b
        c = m.c
    end
    

    这个宏可以在任何函数内部使用,例如:

    function f(m::MyModel)
        @unpack_MyModel(m)
        return a+b
    end
    

    【讨论】:

      【解决方案4】:

      我认为有一种更惯用的方法,不需要宏或任何其他包。文档建议使用最惯用的方法,例如 Matrix factorization 的文档。

      尤其是这一行:

      julia> l, u, p = lu(A); # destructuring via iteration
      

      这表明迭代是正确的方法。所以剩下的就是让你的结构实现iteration interface

      一个非常简单但不惯用的例子:

      你的结构看起来像这样

      struct MyModel
          a::Int = 5
          b::Float64 = 5.5
          c::Matrix{Float64} = rand(3,4)
          # 40 other parameters go here
      end
      

      要实现迭代接口,您需要定义Base.iterate,它将MyModel 作为参数和“状态”。该函数返回与状态对应的元素,并调用下一次迭代(有点像链表)。例如:

      function Base.iterate(m::MyModel, state)
          if state == 1
              return(m.a, state+1)
          elseif state == 2
              return(m.b, state+1
          elseif state == 3
              return(m.c, state+1)
          else
              return nothing
          end
      end
      

      在 julia 中,当 next(iter) == nothing 时迭代停止,这就是为什么当没有任何东西可以迭代时你必须返回 nothing

      高级示例

      您可以在source code 中找到一个更惯用(但做作)的示例,用于lu 分解:

      # iteration for destructuring into components
      Base.iterate(S::LU) = (S.L, Val(:U))
      Base.iterate(S::LU, ::Val{:U}) = (S.U, Val(:p))
      Base.iterate(S::LU, ::Val{:p}) = (S.p, Val(:done))
      Base.iterate(S::LU, ::Val{:done}) = nothing
      

      如果我没记错的话,它使用Val 进行一些编译时优化。

      在具有 40 个字段的结构的特殊情况下,我想不出更符合人体工程学的方式,也许数据更适合另一种存储选项。

      【讨论】:

        猜你喜欢
        • 2017-11-08
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-08-02
        • 2020-08-22
        相关资源
        最近更新 更多