How to unpack a Julia struct entirely into local variables?
Asked Answered
S

4

5

I have created a complex computational model with lots of parameters. Since I need to run many scenarios, I have decided to wrap all those input parameters into one huge 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

I have an object m for an example:

m = MyModel(a=15)

Now when writing mathematical code I do not want to write m. in front of each symbol. Hence I need to make struct fields into local variables. One way is to to use @unpack macro:

@unpack a, b, c = m

For huge structs that I want to unpack in various functions this is just inconvenient (note that my struct has around 40 fields). How can I unpack the struct without spending time and cluttering my code with all those parameters?

Shippee answered 17/4, 2021 at 13:35 Comment(0)
C
4

The macro @with_kw from Parameters.jl defines a macro for this purpose:

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

Thus writing @unpack_MyModel m is equivalent to writing @unpack a, b, c = m, when you know that m isa MyModel.

Chorea answered 17/4, 2021 at 14:2 Comment(3)
Thanks! this indeed saves the time compared to writing macro yourself!Shippee
I guess this is one reason to want the package over Base.@kwdef struct MyModel, which does not do this.Chorea
My favorite reason in the nicer visualization for structs when you show them in the console :-) Anyway ?@with_kw does not state that this nice macro gets implemented.Shippee
S
2

For custom cases, you can make a dedicated macro for your type, for other cases see the answers above:

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

This simply inserts the following code:

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

This macro can be used inside of any function such as:

function f(m::MyModel)
    @unpack_MyModel(m)
    return a+b
end
Shippee answered 17/4, 2021 at 13:35 Comment(0)
B
2

Another option is StaticModules.jl. Here's an example that I copied and pasted from the README for that package:

julia> struct Bar
           a
           b
       end

julia> @with Bar(1, 2) begin
           a^2, b^2
       end
(1, 4)
Balance answered 17/4, 2021 at 14:16 Comment(2)
Usually my computational codes are quite big and often contain other macro calls (eg. because of using JuMP). I wonder how robust is @with with regards to complex code blocks.Shippee
Good question. I haven't actually used StaticModules.jl myself. :)Balance
E
1

I think there's a much more idiomatic approach that doesn't require macros or any other packages. The documentation suggests the most idiomatic approach, for instance the docs for Matrix factorization.

In particular, this line:

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

which suggests iteration is the right way to go. So all that is left is to make your struct implement the iteration interface

A really simple but un-idiomatic example:

Your struct looks like this

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

To implement the iteration interface you need to define Base.iterate which takes MyModel as a parameter and a "state". That function returns the element that corresponds to the state, and calls the next iteration (sort of like a linked list). For example:

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

In julia an iteration stops when next(iter) == nothing, that's why you have to return nothing when there is nothing left to iterate over.

Advanced example

You can find a much more idiomatic (but contrived) example in the source code for the lu factorization:

# 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

which uses Val for some compile-time optimizations if I'm not mistaken.

In the particular case of a struct with 40 fields I can't think of a more ergonomic way, maybe the data is better suited for another storage option.

Ehlers answered 13/10, 2021 at 20:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.