We have builtin syntax sugar for declaring NamedTuples 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 NamedTuples 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?