Kwargs in new(), or safer ways of constructing immutable structs

When creating an immutable struct with a reasonable number of fields and an inner constructor, I find that aligning the arguments to new() with the order of the struct fields to be quite tedious and error prone (especially if adjacent fields have the same type, or I add / reorder types). With a mutable struct, I tend to do the following:

ret = new()
ret.x = x
ret.y = y
ret.z = z
...
return ret

Which is much safer and easier to maintain.

Does anyone have a nice approach to this? It seems like new() doesn’t support kwargs, but it would be cool if it did. I wonder if a macro could be written that would allow new() to take kwargs?

Yes, this is something a @kwnew struct Foo .... end macro could do.

1 Like

Here’s a slight variation on what you asked:

macro construct(T)
    dataType = Core.eval(__module__, T)
    esc(Expr(:call, T, fieldnames(dataType)...))
end

struct Foo
    a
    b
    c
    d
end

function Foo(;
    a = nothing,
    b = 1,
    c = [1,2,3],
    d = 3.4
    )
    @construct(Foo)
end
julia> Foo()
Foo(nothing, 1, [1, 2, 3], 3.4)

julia> Foo(b=18)
Foo(nothing, 18, [1, 2, 3], 3.4)

That said, having a lot of fields in a struct may be a code smell.

Reorganizing some of them into smaller structs and then composing is usually worth it.

Have you looked at Parameters.jl?

1 Like

This is quite creative! And it works fine on the inner constructor by changing T to :new. I think the implicit naming of all the parameters is actually nicer than having to write out all the argument names as kwargs, which likely have the same names as the arguments anyway.

I’ve seen Parameters.jl, though it doesn’t seem to support the kwargs at the level of the inner constructor. Thanks for the help though!

Yes, I was waiting for someone to chime in with this… I find these problems arise even with reasonably sized structs, say with 6 or so fields, especially when they all have the same type.

I wonder if, when calling a similar macro from an inner constructor, there would even be a way to reach up and automatically get the name of the type without explicitly stating it?

Sorry, I don’t understand what type has to do with this.

Having adjacent fields with the same datatype makes a field misordering error silent. If the fields all have unique types, these kinds of errors are detected by the type system.

I guess part of what I was saying is that maybe you could use outer constructors instead of inner constructors, so then you don’t need new().

If you want default values, then create outer constructor with those values.

When calling a constructor, for most purposes I don’t think you could distinguish between inner and outer constructors?

macro construct(T)
    dataType = Core.eval(__module__, T)
    esc(Expr(:call, T, fieldnames(dataType)...))
end

struct Foo
    a
    b
    c
    d
end

function Foo()
    c = [1,2,3]
    a = nothing
    d = 3.4
    b = 1
    @construct(Foo)
end
julia> Foo()
Foo(nothing, 1, [1, 2, 3], 3.4)
1 Like