# Experiment: NamedTuple Syntax w/ Type Declarations

**URL:** <https://discourse.julialang.org/t/experiment-namedtuple-syntax-w-type-declarations/91988>\
**Category:** Internals & Design\
**Tags:** syntax, experimentation, namedtuple\
**Created:** [December 22, 2022, 2:45am UTC](https://discourse.julialang.org/t/experiment-namedtuple-syntax-w-type-declarations/91988 "2022-12-22T02:45:45Z")\
**Posts on this page:** 4\
**Page:** 1

<div class="post-metadata">

**Author:** ![uniment](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/uniment/32/24532_2.png) [@uniment](https://discourse.julialang.org/u/uniment)\
**Post date:** [December 22, 2022, 2:45am UTC](https://discourse.julialang.org/t/experiment-namedtuple-syntax-w-type-declarations/91988/1 "2022-12-22T02:45:45Z")

</div>

We have builtin syntax sugar for declaring `NamedTuple`s like so:

```julia
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
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
> 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
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
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
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?

---

<div class="post-metadata">

**Author:** ![uniment](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/uniment/32/24532_2.png) [@uniment](https://discourse.julialang.org/u/uniment)\
**Post date:** [December 22, 2022, 2:50am UTC](https://discourse.julialang.org/t/experiment-namedtuple-syntax-w-type-declarations/91988/2 "2022-12-22T02:50:18Z")

</div>

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

```julia
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))

```

---

<div class="post-metadata">

**Author:** ![stevengj](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/stevengj/32/71_2.png) [@stevengj](https://discourse.julialang.org/u/stevengj)\
**Post date:** [December 22, 2022, 3:03am UTC](https://discourse.julialang.org/t/experiment-namedtuple-syntax-w-type-declarations/91988/3 "2022-12-22T03:03:41Z")

</div>

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](https://docs.julialang.org/en/v1/manual/performance-tips/#Avoid-fields-with-abstract-type), so I’m not sure it’s urgent to create a compact syntax for this.

---

<div class="post-metadata">

**Author:** ![uniment](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/uniment/32/24532_2.png) [@uniment](https://discourse.julialang.org/u/uniment)\
**Post date:** [December 22, 2022, 3:27am UTC](https://discourse.julialang.org/t/experiment-namedtuple-syntax-w-type-declarations/91988/4 "2022-12-22T03:27:57Z")

</div>

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 😉). 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 `NamedTuple`s 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 😅
