How to properly nest parametrically typed structs while using @kwdef

How to use @kwdef with parametric structs and default constructors?

I’m trying to understand how to best use @kwdef with parametric structs in Julia, particularly when I want to define some default constructors and have those types nested in other custom types eventually.

Here’s an example. I’m defining some AbstractThing types, and then composing them into a larger struct A_bigger_thing{T}. I want to use @kwdef to provide defaults, but I’m not sure I’m doing this in the right way.

MWE

abstract type AbstractThing end

@kwdef struct Thing1 <: AbstractThing 
    x::Float64 = 0.0
end

@kwdef struct Thing2 <: AbstractThing 
    x::Float64 = 0.0
    y::Float64 = 1.0
end

# If I define this with @kwdef, I run into problems
# So I leave @kwdef off and write a manual outer constructor
mutable struct A_bigger_thing{T<:AbstractThing}
    thing::T
    value::Float64
end

# This works:
function A_bigger_thing{T}() where {T<:AbstractThing}
    A_bigger_thing{T}(T(), 0.0)
end

# Example usage:
# julia> A_bigger_thing{Thing1}()
# A_bigger_thing{Thing1}(Thing1(0.0), 0.0)

# julia> A_bigger_thing{Thing2}()
# A_bigger_thing{Thing2}(Thing2(0.0, 1.0), 0.0)

All this is fine, but I’m getting confused a little when I start to use @kwdef for convenience (in my real application there are 15 other fields that I provide defaults in the type definition).

For example, if I define Another_bigger_thing using @kwdef:

@kwdef mutable struct Another_bigger_thing{T<:AbstractThing}
    thing::T
    value::Float64 = 1.0
end

I can instantiate them like:

julia> Another_bigger_thing(thing = Thing1())
Another_bigger_thing{Thing1}(Thing1(0.0), 1.0)

julia> Another_bigger_thing(thing = Thing2())
Another_bigger_thing{Thing2}(Thing2(0.0, 1.0), 1.0)

But what I’d like is to be able to write is: Another_bigger_thing{Thing1}() because I want this to be a field in another struct, for example:

@kwdef mutable struct An_even_bigger_thing{T<:AbstractThing}
    value1::Float64 = 0.0
    value2::Float64 = 1.0
    big_thing::Another_bigger_thing{T} = Another_bigger_thing{T}()
end

So my approach was to define:

function Another_bigger_thing{T}() where {T<:AbstractThing}
    Another_bigger_thing{T}(; thing = T())
end

My Question

This seems to work, but I’m wondering:

  • Is this a good way to combine @kwdef with parametric structs and default constructors?
  • Is there a better way to support default construction (e.g., Another_bigger_thing{T}()) while still using keyword defaults via @kwdef?
  • Are there any subtle pitfalls to this approach when used in larger codebases?

Thanks in advance for any guidance or best practices here!