I carried out a little study to better understand type-stable overloading of getproperty and getindex with NamedTuple by using Val. Please see the (more or less) MWE below.
I think I understand almost everything in that script, except why the second to last example is type stable:
function test_symbols(wrapper, keys)
for k in keys
@inferred wrapper[Val(k)]
end
end
test_symbols(wr, (:t, :x))
Why has passing several keys in a Tuple and wrapping them into Val in a loop an advantage over passing just a single key as Symbol and wrapping it into Val as in the last example (where the wrapping happens in getindex)?
I see why the last example is not type stable, but I do not see why the second to last example is type stable.
using Test
# The type `Wrapper` has only one field that is a `NamedTuple`
struct Wrapper{wrapperType<:NamedTuple}
data::wrapperType
end
function Wrapper(data::NamedTuple)
Wrapper{typeof(data)}(data)
end
# `hasproperty` checks if `s` is either a key of the `NamedTuple` or a field in `Wrapper`
function Base.hasproperty(::Wrapper{WT}, s::Symbol) where {WT}
hasfield(WT, s) || hasfield(Wrapper, s)
end
# `getproperty` checks if `s` is a key of the `NamedTuple` and if so returns the corresponding value,
# if not it returns the corresponding field of `Wrapper`
function Base.getproperty(w::Wrapper{WT}, s::Symbol) where {WT}
if hasfield(WT, s)
return getfield(getfield(w, :data), s)
else
return getfield(w, s)
end
end
# Return wrapped NamedTuple
Base.parent(w::Wrapper) = w.data
# This getindex method recieves the key symbol wrapped in Val,
# which implies that the value is known at compile time
Base.getindex(w::Wrapper, ::Val{s}) where {s} = getindex(parent(w), s)
# This getindex method wraps the key symbol into a Val, and passes it on to the above method
Base.getindex(w::Wrapper, s::Symbol) = getindex(w, Val(s))
### Test for type stability ###
# Create a `Wrapper` instance based on some arbitrary data (with different types)
data = (
t=0.0,
x=rand(3),
)
wr = Wrapper(data)
# Accessing fields of NamedTuple via forwarding in getproperty is type-stable
# as the value of the key symbol is known at compile time:
test_t(wrapper) = wrapper.t
test_x(wrapper) = wrapper.x
@inferred test_t(wr)
@inferred test_x(wr)
# Accessing fields of NamedTuple via getindex of Val{<:Symbol} is type stable
test_symbol(wrapper, s) = wrapper[s]
@inferred test_symbol(wr, Val(:t))
@inferred test_symbol(wr, Val(:x))
@inferred wr[Val(:t)]
@inferred wr[Val(:x)]
# Accessing fields of NamedTuple via getindex of Val{<:Symbol} with
# Val wrapping inside a loop is also type stable
function test_symbols(wrapper, keys)
for k in keys
@inferred wrapper[Val(k)]
end
end
test_symbols(wr, (:t, :x))
# Accessing fields of NamedTuple via getindex of Symbol that wraps the key
# symbol into a Val is not type stable
@inferred test_symbol(wr, :t)
@inferred test_symbol(wr, :x)
This was tested on
Julia Version 1.13.0-beta2
Commit f36fbbfd951 (2026-02-04 11:07 UTC)
Build Info:
Official https://julialang.org release
Platform Info:
OS: macOS (arm64-apple-darwin24.0.0)
CPU: 16 × Apple M4 Max
WORD_SIZE: 64
LLVM: libLLVM-20.1.8 (ORCJIT, apple-m4)
GC: Built with stock GC
Threads: 1 default, 1 interactive, 1 GC (on 12 virtual cores)