We have builtin syntax sugar for declaring NamedTuple
s like so:
julia> a, b, c, d = 1, 2, 3, 4
(1, 2, 3, 4)
julia> (; a=1, b=2, c, d)
(a = 1, b = 2, c = 3, d = 4)
However, if we wish to declare types (e.g. :a
as Number
and :c
as Any
), then we must either use the constructor, or use the @NamedTuple
macro:
julia> NamedTuple{(:a, :b, :c, :d), Tuple{Number, Int, Any, Int}}((1, 2, c, d))
NamedTuple{(:a, :b, :c, :d), Tuple{Number, Int64, Any, Int64}}((1, 2, 3, 4))
julia> @NamedTuple{a::Number, b::Int, c, d::Int}((1, 2, c, d))
NamedTuple{(:a, :b, :c, :d), Tuple{Number, Int64, Any, Int64}}((1, 2, 3, 4))
Neither of these is particularly ergonomic though, and the macro still requires a pretty heavy mental load.
Let’s try an experiment.
Here's a short macro.
julia> function nt!(ex)
_name(p) = if Meta.isexpr(p, :(::)) p.args[1]
elseif Meta.isexpr(p, :kw)
if Meta.isexpr(p.args[1], :(::)) p.args[1].args[1]
else p.args[1] end
else p end
_hasval(p) = Meta.isexpr(p, :kw)
_val(p) = if _hasval(p) p.args[2] else _name(p) end
_type(p) = if Meta.isexpr(p, :(::)) p.args[2]
elseif _hasval(p) && Meta.isexpr(p.args[1], :(::)) p.args[1].args[2]
else :(typeof($(_val(p)))) end
_nt!(ex) = ex
_nt!(ex::Expr) = let
if Meta.isexpr(ex, :tuple) && Meta.isexpr(ex.args[1], :parameters) let p=ex.args[1]
newex = :(NamedTuple{$(map(_name, Tuple(p.args))), Tuple{$(map(_type, p.args)...)}}(($(map(_val, Tuple(p.args))...),)))
ex.head = newex.head
ex.args = newex.args
end end
Meta.isexpr(ex, :(=)) ? map(_nt!, ex.args[2:end]) : map(_nt!, ex.args)
end
ex isa Expr && _nt!(ex)
ex
end
nt! (generic function with 1 method)
julia> macro nt(ex) esc(nt!(ex)) end
@nt (macro with 1 method)
We can invoke it like so:
julia> @nt my_nt = (; a::Number=1, b=2, c::Any, d)
NamedTuple{(:a, :b, :c, :d), Tuple{Number, Int64, Any, Int64}}((1, 2, 3, 4))
We can also clean up the show
method while we’re at it:
julia> Base.show(io::IO, nt::T) where {T<:NamedTuple} = print(io, "(;"*join((" $k"*(typeof(nt[k])===t ? "" : "::$t")*" = $(nt[k])" for (k,t) ∈ zip(fieldnames(T), fieldtypes(T))),", ")*")")
julia> @nt (; a::Number=1, b=2, c::Any, d)
(; a::Number = 1, b = 2, c::Any = 3, d = 4)
This way, show
only shows types when the value’s concrete type isn’t the same as the field type (i.e., when the field type is abstract).
Then we can declare NamedTuple
s with field types quite ergonomically, and type promotions occur naturally. For example:
julia> (; a=a::Float64, b=b::Complex{Int})
ERROR: TypeError: in typeassert, expected Float64, got a value of type Int64
julia> @nt (; a::Float64, b::Complex{Int})
(; a = 1.0, b = 2 + 0im)
Thoughts?