How to copy the fields of a structure as new local variables?

Let’s say I have a loooong Parameters structure and a function where I want to refer simply with the parameter names rather than something like p.parameterx, how can I “automatically” copy the struct fields as local parameters ?

This doesn’t work because of course eval happens at global scope:


@kwdef mutable struct Parameters
    a::Float64 = 0.1
    b::String  = "hello"
end

function bar()
    p = Parameters()
    #  create local variables a == 0.1 and b= "hello"
    fields = fieldnames(Parameters)
    for f in fields
        println(getproperty(p, f))
        eval(:(local $f = $(getproperty(p, f))))
    end
    println("$b $a !")
end

hmm… it seems it’s not possible, the trick seems to eventually eval the whole function and then call the function, but the whole point it to make the code more readable also to people that doesn’t know Julia…

Is property destructuring an option?

@kwdef mutable struct Parameters
    a::Float64 = 0.1
    b::String  = "hello"
end

function bar()
    p = Parameters()
    (; a, b) = p
    println("$b $a !")
end
4 Likes

Provided the type definition was already evaluated and the type’s name is inlined, a macro call in the method body can automatically generate variables from the field names. An attempt:

macro fields2vars(t::Symbol, x)
    type = Core.eval(__module__, t)
        if !isstructtype(type)
            throw(ArgumentError("@fields2vars only takes struct types, not $type."))
        end 
    esc(:( (; $(fieldnames(type)...)) = $x::$type ))
end

function bar2()
    p = Parameters()
    @fields2vars Parameters p
    println("$b $a !")
end

But since you’ll have to manually type the variables after the macro call to use them anyway, typing each used variable 1 more time doesn’t seem much more effort than this macro call, and the macro call constrains the code more because it needs a specific inlined type by definition-time to generate the local variables. In practice, you may not know the type even by compile-time, and property destructuring can be attempted at runtime.

1 Like

Thank you… it works in my user-case, where I have compiled struct with default values but want let users the option to override them:

@kwdef mutable struct Parameters
    a::Float64 = 0.1
    b::String  = "hello"
end

macro fields2vars(t::Symbol, x)
    type = Core.eval(__module__, t)
        if !isstructtype(type)
            throw(ArgumentError("@fieldvars only takes struct types, not $type."))
        end 
    esc(:( (; $(fieldnames(type)...)) = $x::$type ))
end

function bar2(;kwargs...)
    p = Parameters()
    # here keyword arguments are used to override the default values...
    for (kw,kwv) in kwargs
        found = false
        if kw in fieldnames(Parameters)
            setproperty!(p,kw,kwv)
            found = true
        end
        found || error("Keyword \"$kw\" is not a valid parameter.")
    end
    @fields2vars Parameters p
    println("$b $a !")
end

bar2()
bar2(a=0.2)

You don’t need that mutating for-loop, @kwdef is designed to allow you to instantiate with compatible keyword arguments and input types, e.g. p = Parameters(;kwargs...). The only practical difference is the custom error message for incompatible keywords being specific to bar2 rather than the internal Parameters, and if you want to keep that, you can check issubset(keys(kwargs), fieldnames(Parameters)) before you instantiate at all.

1 Like