Experiment: NamedTuple Syntax w/ Type Declarations

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?

1 Like

Per tradition, this works recursively so you can do this:

julia> using REPL

julia> pushfirst!(Base.active_repl_backend.ast_transforms, nt!);

julia> my_nt = (; a::Float64, b::Complex{Int}, c=(; d::Complex{Float64}))
(; a = 1.0,  b = 2 + 0im,  c = (; d = 4.0 + 0.0im))

The main motivation for the @NamedTuple macro was to declare types, not for constructing instances, e.g. if you want a struct field to be a @NamedTuple type, it is nice to have a struct-like syntax to declare the type.

What is the use case for constructing named tuple instances with abstractly typed fields? Usually, this is something you’d want to avoid, so I’m not sure it’s urgent to create a compact syntax for this.

1 Like

I think you’re right, there’s probably no urgent need to make a compact syntax; I’m just experimenting with macros for practice and fun (Julia does make playing with macros a bit too much fun :wink:). For instances with promoted types it’s easy enough to write (; a=Float64(a), b=Complex(b)), so the only real thing such a syntax would offer is abstract types.

I’ve been working with NamedTuples whose fields are programmatically typed quite a bit lately, and I never found the @NamedTuple macro to offer what I was looking for so I guess I assumed the motivation for the macro was more directed toward instances instead of constructors. But that’s probably just because I’m declaring field types programmatically :sweat_smile:

1 Like