Construct an immutable type from a Dict

In development, I sometimes have an immutable type (for some reason, I do not want it to be mutable) with a large number of fields (5 in the following example), like

struct A
   mass::Int
   velocity::String
   coordinate::Vector
   time::Tuple
   frequency::Float64
end

Please do not consider this question as a physics problem, the fieldnames I used here are just to say they can be long enough that we need a Dict to “remember” them.
Now suppose I have a Dict of fields (read from a file, for example),

Dict(
    :mass => 1,
    :velocity => "2.0",
    :coordinate => [3, 3, 3],
    :time => (2, 3),
    :frequency => 5.0
)

How can I construct an A type from the Dict?

Something in your explanation doesn’t really fit with other parts…

Why is that? For one, since computer doesn’t really care about the length of the field name I assume this is to help a human (user or developer) to remember them. The computer only care about the length regarding memory consumption and a Dict certainly doesn’t help in that regard. Moreover, I don’t think a Dict can really help you remember it either. Are you creating them, in which case you have to type the field names. Or are you getting it from somewhere, in which case the reader code should have no problem remembering the field names and you shouldn’t have to put them into a Dict to “remember them”.

So if you are reading from a file, I still don’t really see how Dict is essential here. The Dict is at most a slightly different representation of what you get from the file so are you just saying that you are using a Dict as a generic intermediate format to store the info that you later want to put into your type? Depending on the design, you could be better off not have this step but having this is certainly a valid choice. Is that what you want? (Even if this is the case I still don’t really see how it helps you remember though, especially since there isn’t really any alternative that does require you to remember the field names…)

What I said above doesn’t really affect the way you construct A but it’ll strongly affect whether you even need to do these and how do you integrate the code if you do need it.

Now onto creating A. The only way you can create an immutable type is from it’s constructor so if you want to make any generic code you must make some assumptions about the constructor. The only one you can make without special convention in your own code is to assume it has something similar to the default constructor, in which case the only way to do what you want is pretty much A(d[:mass], d[:velocity], d[:coordinate], d[:time], d[:frequency]) and variances using get etc to handle missing/default values. You can get the effect of the expression above in many different ways. If this is something that is type stable (i.e. the caller know the type) you can use generated function and generate the expression above based on the input type, sth like this,

julia> :(A($((:(d[$(QuoteNode(f))]) for f in fieldnames(A))...)))
:(A(d[:mass], d[:velocity], d[:coordinate], d[:time], d[:frequency]))

if this is sth like deserialization where the caller doesn’t really know the type (the type is also read from the file, for example) then you can just construct the arguments directly, sth like,

julia> A((d[f] for f in fieldnames(A))...)
A(1, "2.0", [3, 3, 3], (2, 3), 5.0)

which one to pick is strongly depending on what you are doing, as mentioned above.

P.S. many of your field types for A are not concrete types and you may want to change that.

1 Like

Hi @yuyichao, thank you for your great help! I should have clarified on what I want more detailedly. The example I set is just an MWE, so it might not represent my use case precisely, which confuses.

Indeed, it was intended to help human “remember”, like what a namedtuple is to a tuple.

Yes, I read a Fortran namelist, which looks like a dictionary. And I have several types representing those namelists, some of them could have a hundred fields, which you definitely do not want to create from scratch but from a template, and modify some fields each time creating one. That uses a Dict. Check my code if you are interested.

Kind of. I want to create one from my template, and merge the fields with my Dict's entries to create a new Namelist.

I have thought about using a macro. I tried several times but failed. Thank you for your solution.

It’s a shame that I did not come up with a generator. My focus was on how to make a macro. Thank you!

I am aware of that, it is just an example. :slight_smile:

I think you may want to look for something like Base.@kwdef or, if you need more functionality (unpacking, conversion to/from Dict), look at https://github.com/mauro3/Parameters.jl.

1 Like

Thanks, actually I am using Parameters.jl. But it cannot construct a type from a Dict directly, it needs an instance of that type first.

MM(m::MM; kws...) = reconstruct(mm,kws)
MM(m::MM, di::Union{AbstractDict, Tuple{Symbol,Any}}) = reconstruct(mm, di)

Therefore, if I want to create an instance from a Dict, I need to construct an instance first by giving it some random parameters. However, I always have trouble determining what values should be given.

This works:

julia> using Parameters

julia> @with_kw struct A
       a
       b
       end
A

julia> A(;Dict(:a=>1, :b=>3)...)
A
  a: Int64 1
  b: Int64 3

Does this cover your use-case?

2 Likes

Right. It covers. I was not thinking of unzipping a generator or a Dict. I forgot there was a method

MM(;r=1000,a=error("no default for a")) =  MM(r,a) # outer kw, so no type-paras are needed when calling

Thank you for all of your help! @mauro3 @yuyichao

1 Like

If the Dict is mapping symbols to values, you can simply do array comprehensions:

julia> struct A
          mass::Int
          velocity::String
          coordinate::Vector
          time::Tuple
          frequency::Float64
       end

julia> d = Dict(
           :mass => 1,
           :velocity => "2.0",
           :coordinate => [3, 3, 3],
           :time => (2, 3),
           :frequency => 5.0
       )

julia> A([d[name] for name in fieldnames(A)]...)
A(1, "2.0", [3, 3, 3], (2, 3), 5.0)

Yes, thank you. This was also pointed out in @yuyichao’s answer.