Using incompletely initialized structs instead of fields of type Union{Nothing,T}

Suppose I want to define a custom type that I initialize gradually one field at a time. I could do something like this:

mutable struct A
    x::Union{Nothing, Int}
    y::Union{Nothing, Bool}
    z::Union{Nothing, String}
end

A() = A(nothing, nothing, nothing)

a = A()
a.x = 1
a.y = true
a.z = "hello"

However, I’m tempted to use an uninitialized struct instead, since it looks much cleaner:

mutable struct B
    x::Int
    y::Bool
    z::String
    B() = new()
end

b = B()
b.x = 1
b.y = true
b.z = "hello"

Is there anything wrong with the uninitialized struct approach that I take with my type B? I’m aware that accessing an uninitialized field raises an UndefRefError exception, but that seems acceptable as long as I’m only using this for my own internal code (i.e. it’s not part of a public API).

The risk is that you may inadvertently omit initializing a field some time. If your dataflow is such that you are sure to have all initializing field values available before utilizing the [initialized] struct, then it may be easy to refactor the logic so you do b = B(xforb, yforb, zforb) after having established xforb, yforb, and zforb rather than before. That way, the first method is not needed … and would buy you nothing because if you can do the second method then you are not using nothing values (or so it appears).

2 Likes

Yeah, that makes sense. That’s what I normally do. However, I have a scenario where it’s easier to initialize the fields gradually. But perhaps with some careful thought I might be able to refactor. It’s a bit of a spaghetti code where I’m dynamically building up a graph with meta-data while making HTTP requests, handling errors, etc… :joy:

This is not quite true. In your case the x::Int field will always be set (to some garbage in general). This is true for all bits types. This is a great way to introduce very subtle bugs. I would at least always set the fields of bits types to some sentinel values. This way you can still distinguish between “forgot to set it” and “set it”. If you can’t use a sentinel value, I would refrain from doing this.

6 Likes

Kristoffer has this nice package: https://github.com/KristofferC/LazilyInitializedFields.jl

Maybe it would work for your use case?

3 Likes

Another option if for performance reasons you wanted your struct to be immutable instead of mutable (while keeping type stability) is the following combination of Base.@kwdef and Setfield.@set!, which leads to not-too-bad-looking code.

Base.@kwdef struct A{X<:Union{Nothing,Int}, Y<:Union{Nothing,Bool}, Z<:Union{Nothing, String}}
    x :: X = nothing
    y :: Y = nothing
    z :: Z = nothing
end

a = A()
@set! a.x = 1
@set! a.y = true
@set! a.z = "hello"
1 Like

Good point. I guess here I am somewhat dubiously relying on my skill as a programmer to not access fields before they are initialized. :slightly_smiling_face:

Oh, nice! That looks cool. Thanks for the link!

Thanks, that’s an interesting approach! I should take a closer look at Setfield.jl.

I think it is best practice to make sure all the fields are initialized (or at least consistent, depending on the application) once the constructor is done.

Inside the constructor, you pretty much do what you like, but if an object with unassigned fields is considered valid for some purposes, the Union{Nothing,T} approach is preferable.

3 Likes