\begin{section}{title="Lazily initialized fields", name="About"}
A lazily initialized field is a field in a struct that starts off uninitialized
(does not have a value) and at some later point gets initialized. This is useful
when the value of this field is computed on-demand (lazily).
Some goals we want to achieve when using lazy fields:
1. Accessing a lazily initialized field before it is initialized should error immediately.
2. Using a lazily initialized field does not interfere with the inferred return
value of the field.
3. The struct should act as similar as possible to the equivalent normal struct when the struct is fully initialized.
4. Make it possible to uninitialize a field after it has been initialized if the value becomes invalidated for some reason.
5. Not force all fields to be considered mutable just because we want to lazily initialize one field.
6. Allow checking if a field is initialized.
\end{section}
\begin{section}{title="Installation"}
```julia
using Pkg; Pkg.add("LazilyInitializedFields")
```
\end{section}
\begin{section}{title="Usage", name="Usage"}
Let's see a session with LazilyInitializedFields and how these goals are
fulfilled. We first define a struct with one lazily initialized field.
We then create it, using the exported `uninit` object for the field that
should be lazily initialized:
```julia-repl
julia> @lazy struct Foo
a::Int
@lazy b::Int
end
julia> f = Foo(1, uninit)
Foo(1, uninit)
```
1. Accessing a lazily initialized field before it is initialized should error immediately.
```julia-repl
julia> f.b
ERROR: uninitialized field b
```
2. Using a lazily initialized field does not interfere with the inferred return value of the field.
```julia-repl
julia> @code_warntype (f -> f.b)(f)
Variables
#self#::Core.Compiler.Const(var"#1#2"(), false)
f::Foo
Body::Int64
1 ─ %1 = Base.getproperty(f, :b)::Int64
└── return %1
```
3. The struct should act as similar as possible to the equivalent normal struct when the struct is fully initialized.
```julia-repl
julia> @init! f.b = 2
2
julia> f.b
2
```
4. Make it possible to uninitialize a field after it has been initialized if the value for example becomes invalidated.
```julia-repl
julia> @uninit! f.b
uninit
julia> f.b
ERROR: uninitialized field b
```
5. Not force all fields to be considered mutable just because we want to lazily initialize one field.
```julia-repl
julia> f.a = 2
ERROR: setproperty! for struct of type `Foo` has been disabled
```
6. Allow checking if a field is initialized.
```julia-repl
julia> @isinit f.b
false
julia> @init! f.b = 2
2
julia> @isinit f.b
true
```
Instead of the macros `@init! a.b = 1`, `@isinit a.b` and `@uninit! a.b` one can
use the function `init(a, :b, 1)`, `isinit(a, :b)` and `uninit!(a, :b)`.
\end{section}
\begin{section}{title="Other methods of achieving lazily initialized fields", name="Other methods"}
Let's assume we want to make a struct `Foo` with two `Int` fields, and the
second field is lazily initialized. Here are some other more or less used
methods other than using LazilyInitializedFields.jl:
~~~
Use a ::Ref{T}
field
~~~
This does not work for `isbitstype` fields and we also need to use `[]` to
access the value, thus, failing points 1 and 3 above.
```julia-repl
julia> mutable struct Foo
a::Int
b::Ref{Int}
end
julia> Foo(a) = Foo(a, Ref{Int}());
julia> f = Foo(1)
Foo(1, Base.RefValue{Int64}(4764233584))
julia> f.b
Base.RefValue{Int64}(4764233584)
julia> f.b[]
4764233584
```
~~~Make struct mutable together with new
initialization
~~~
This also does not work for `isbitstype` and for non-`isbitstype` we cannot
uninitialize the field, failing points 1, 4 and 5 above.
```julia-repl
julia> mutable struct Foo
a::Int
b::Int
Foo(a) = new(a)
end
julia> f = Foo(1)
Foo(1, 29548)
julia> f.b
29548
```
~~~Make struct mutable and use a Union{T, Nothing}
~~~
Accessing this field will not error when it is uninitialized and will infer as a
union when the field is accessed, failing points 1, 2 and 5 above.
```
julia> mutable struct Foo
a::Int
b::Union{Nothing, Int}
end
julia> f = Foo(1, nothing)
Foo(1, nothing)
julia> f.b # no error
julia> @code_warntype (f -> f.b)(f)
Variables
#self#::Core.Compiler.Const(var"#1#2"(), false)
f::Foo
Body::Union{Nothing, Int64}
1 ─ %1 = Base.getproperty(f, :b)::Union{Nothing, Int64}
└── return %1
```
\end{section}
\begin{section}{title="Caveats"}
When applying `@lazy` to a non-mutable struct, the standard way of mutating it
via `setproperty!` (the `f.a = b` syntax) is disabled. However, the struct is
still considered mutable to Julia and the `setproperty!` can be bypassed:
```julia-repl
julia> @lazy struct Foo
a::Int
@lazy b::Int
end
julia> f = Foo(1, uninit)
Foo(1, uninit)
julia> f.a = 2
ERROR: setproperty! for struct of type `Foo` has been disabled
[...]
julia> setfield!(f, :a, 2)
2
julia> f.a
2
```
The fact that the struct is considered mutable by Julia also means that it will
no longer be stored inline in cases where the non `@lazy` version would:
```julia-repl
julia> isbitstype(Foo)
false
```
This has an effect if you would try to pass a `Vector{Foo}` to e.g. C via `ccall`.
\end{section}
\begin{section}{title="Implementation"}
The expression
```julia
@lazy struct Foo
a::Int
@lazy b::Int
end
```
expands to three or four parts (in the case where the struct is non-mutable). To
make the code below runnable, we define the type `Uninitialized` that in reality lives
inside `LazilyInitializedFields`:
```
struct Uninitialized end
const uninit = Uninitialized()
```
The first part of the expanded macro is the struct definition:
```julia
mutable struct Foo
a::Int
b::Union{Uninitialized, Int}
end
```
This allows us to store a custom sentinel singleton that always signals an
undefined value. The struct has also been made mutable since otherwise we cannot
change the uninitialized value. The second part is to extend a method in
LazilyInitializedFields that can be used to query what fields are lazy:
```julia
islazyfield(::Type{<:Foo}, s::Symbol) = s === :b
```
The third part is `getproperty` overloading:
```julia
function Base.getproperty(f::Foo, s::Symbol)
if islazyfield(Foo, s)
r = getfield(f, s)
r isa Uninitialized && error("uninitialized field b")
return r
end
return getfield(f, s)
end
```
This makes sure that accessing an uninitialized field errors *and* that type
inference knows that the return value is exactly an `Int`. Since the struct was
originally non-mutable, we also turn off `setproperty!` via:
```julia
function Base.setproperty!(x::Foo, s::Symbol, v)
error("setproperty! for struct of type `Foo` has been disabled")
end
```
The convenience macros `@init!`, `@uninit!`, `@isinit` does very simple
transformations that checks that the field being manipulated is lazy (via
`islazyfield`) and converts `getproperty` and `setproperty!` to `getfield` and
`setfield!`.
\end{section}