Looking for type-stable way to update several struct fields from NamedTuple

Hello,

I’ve been poring over docs for Accessors.jl, Setfield.jl, Parameters.jl, and Discourse, and haven’t found a clear answer to this question.

The problem I would like to solve is expressed by this MWE:

struct ParamObj
    a
    b
    c
end

function new_params(x, y)
    return (a=x, b=y)
end

po = ParamObj(1.0, 2.0, 3.0)
nt = new_params(4.0, 5.0)

po2 = nt_into_struct(po, nt)
# ParamObj(4.0, 5.0, 3.0)

where this imaginary function nt_into_struct takes all the fields of the NamedTuple and, a la Accessors.jl, produces a new instance where fields not in the NamedTuple come from the base object. Key here is that I may have several different functions producing these NamedTuples, which (I think) are each inferrable themselves but map to different fields of the struct, so I would like to programmatically assess which fields need to be set.

The best I could produce myself, using Accessors, is

function nt_into_struct(po, nt::NamedTuple)
    @warn "Copying arbitrary NamedTuple into struct. Type unstable. If doing this repeatedly, define a new method for copy_nt_into_struct" nt
    po_new = deepcopy(po)
    for (k, v) in zip(keys(nt), values(nt))
        po_new = set(po_new, PropertyLens{k}(), v)
    end
    return po_new
end

which does the job but is type-unstable, which is not acceptable for me since this is going to be in a hot inner loop.
I can write new methods like

function nt_into_struct(po, nt::NamedTuple{(:a, :b)})
    po_new = deepcopy(po)
    @reset po_new.a = nt.a
    @reset po_new.b = nt.b
    return po_new
end

but then I have to write each of these methods manually; I can deal with that but feel like there ought to be a more elegant solution, especially since this will be specific to the ordering of parameters in the NamedTuple.
I can imagine a macro that generates new methods of nt_into_struct like this, but don’t feel confident enough to have tried that yet.

A function like reconstruct, which allows reconstruct(po; nt...) from Parameters.jl would do the trick, but the docs there warn that it is slow and to look at Setfield.jl.

I have the feeling that I am missing something obvious here (like maybe all my structs here should just be NamedTuples, in which case I can use merge.)

I’ve used the following code, still requires one method per struct (unless you know the type won’t change, but I guess you expect that since you mention type stability?)

julia> @kwdef struct ParamObj{TA,TB,TC}
           a::TA
           b::TB
           c::TC
       end

julia> struct_to_namedtuple(x::T) where T = NamedTuple{fieldnames(T)}(tuple((getproperty(x,k) for k in fieldnames(T))...));

julia> nt_into_struct(s::ParamObj, nt) = ParamObj(;struct_to_namedtuple(s)..., nt...)
nt_into_struct (generic function with 1 method)

julia> po = ParamObj(1.0, 2.0, 3.0);

julia> nt = (a = 1, b = "b");

julia> new_po = nt_into_struct(po, nt)
ParamObj{Int64, String, Float64}(1, "b", 3.0)
1 Like

Not a bad idea. In my case I have a small number (maybe 2-3) of structs that would need this method, and a potentially larger number of kinds of NamedTuples, so the burden of adding a new method per type is minimal.

Sounds like you want setproperties(obj, nt) from ConstructionBase.jl.

3 Likes

This is getproperties(x) btw :slight_smile:

1 Like

It makes me so happy to learn these kinds of elegant solutions already exist, and to get an answer within an hour!