Dynamically defining a struct with default values

Hi,

I would like to be able to define a struct with some default values based on an input Dict.

The input is something like this:

d = Dict("A"=>1, "B"=>2, "C"=>"x", "D"=>[1,2])

And I would like to define a struct like below:

@kwdef struct MyStruct3
	A::Int64 = 1
	B::Int64 = 2
	C::String = "x"
        D::Vector{Int64} = [1,2]
end

I’ve got to a point where I can create a struct with type:

macro make_struct(name, dict)
	symbols=[:($(Symbol(k))::$(typeof(v))) for (k, v) in eval(dict)]
	:(@kwdef struct $(esc(name))
    	$(map(esc,symbols)...)
    end)
end

@make_struct test d

But am not sure how to add default values to that, when I tried this:

macro make_struct(name, dict)
	symbols=[:($(Symbol(k))::$(typeof(v))=$(v)) for (k, v) in eval(dict)]
	:(@kwdef struct $(esc(name))
    	$(map(esc,symbols)...)
    end)
end

I got the following error:

Expr[:(B::Int64 = 2), :(A::Int64 = 1), :(C::String = "x"), :(D::Vector{Int64} = [1, 2])]
ERROR: syntax: "B::Int64 = 2" inside type definition is reserved around util.jl:589
Stacktrace:
 [1] top-level scope
   @ REPL[149]:1

Here, I would not use a macro as the code cannot be generated at compile time and you will have to call eval at runtime anyways. The following function works for me:

function def_dyn_struct(name::Symbol, spec::Dict)
    fields = [:($(Symbol(k))::$(typeof(v)) = $(v)) for (k, v) in spec]
    @eval @kwdef struct $(name)
        $(fields...)
    end
end

Your error seems to be related to the fact that A::Int64 = 1 is not valid inside a normal struct definition:

julia> struct Hu
           A::Int64 = 1
       end
ERROR: syntax: "A::Int64 = 1" inside type definition is reserved around REPL[39]:1

Further, when evaluating the code, I guess that esc is unnecessary (and also not handled gracefully by @kwdef):

julia> let name = :Ha
           @eval @kwdef struct $(esc(name)) end
       end
ERROR: syntax: invalid type signature around util.jl:589
2 Likes

Legend! Yeah that works for me - thanks :ok_hand:

Glad you have a solution. FWIW, if you ever wanted full dynamism, NamedTuples are the way to get it. Something like

struct Flexible{NT}
    fields::NT
end
mk_struct_constructor(dict::Dict) = mk_struct_constructor(NamedTuple(dict))
Base.getproperty(f::Flexible, field) = getproperty(f.fields, field)
function mk_struct_constructor(nt::NamedTuple)
    constructor(; kwargs...) = Flexible(merge(nt, NamedTuple(kwargs)))
end

then

julia> MyStruct3 = mk_struct_constructor(Dict(:A=>2))
(::var"#constructor#7"{var"#constructor#6#8"{NamedTuple{(:A,), Tuple{Int64}}}}) (generic function with 1 method)

julia> MyStruct3()
Flexible{NamedTuple{(:A,), Tuple{Int64}}}((A = 2,))

julia> MyStruct3(A=5)
Flexible{NamedTuple{(:A,), Tuple{Int64}}}((A = 5,))

This won’t face the world-age issues you’d have with the previous solution, and it’s AFAIK just as efficient as a normal struct.

3 Likes