Przemyslaw Szufel
Przemyslaw Szufel

Reputation: 42214

How to unpack a Julia struct entirely into local variables?

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?

Upvotes: 5

Views: 2421

Answers (4)

Alonso Martinez
Alonso Martinez

Reputation: 31

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.

Upvotes: 1

Przemyslaw Szufel
Przemyslaw Szufel

Reputation: 42214

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

Upvotes: 2

Cameron Bieganek
Cameron Bieganek

Reputation: 7674

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)

Upvotes: 2

mcabbott
mcabbott

Reputation: 2580

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.

Upvotes: 4

Related Questions