Declaring Types in NamedTuples

Julia 1.8 introduces new syntax to declare types of global variables…

julia> y::Int = 3
3

julia> y = 2.5
ERROR: InexactError: Int64(2.5)

julia> z::Number = π
π = 3.1415926535897...

julia> z = 1+1im
1 + 1im

julia> z = "Hello, world!"
ERROR: MethodError: Cannot `convert` an object of type String to an object of type Number

This syntax is consistent with method argument specification, for optional arguments:

g(x::Int=5) = x^2

I just learned that NamedTuples can contain abstract types and be type-unstable just like structs can, for example:

julia> x = NamedTuple{(:a, :b), Tuple{Number, Int}}((1, 2.0))
NamedTuple{(:a, :b), Tuple{Number, Int64}}((1, 2))

It would seem consistent that when constructing NamedTuples, one should be able to declare the types using similar syntax, but:

julia> x = (a::Number=1, b::Int=2.0)
ERROR: syntax: invalid named tuple field name "a::Number" around REPL[42]:1

Any insights or opinions?

2 Likes

These are very different things that you are comparing, but yes, I suppose that you could have this syntax (probably we need to check for some edge case that is ambiguous) and it to be “Natural”. As you pointed out, however, there is a way to do it already that is just a little more verbose.

Interestingly, the Base.Pairs that is generated by keyword arguments also support overriding concrete types with abstract types.

julia> x = NamedTuple{(:a, :b), Tuple{Number, Int}}((1, 2.0))
NamedTuple{(:a, :b), Tuple{Int64, Number}}((1, 2))

julia> ((; kwargs...)->kwargs)(; x...) |> typeof
Base.Pairs{Symbol, Number, Tuple{Symbol, Symbol}, NamedTuple{(:a, :b), Tuple{Number, Int64}}}

Strangely, however, while the regular Tuple supports changing concrete types, unlike NamedTuple it doesn’t support abstract types.

julia> y = Tuple{Number, Int, Float64}((1, 1.0, 1))
(1, 1, 1.0)

julia> typeof(y)
Tuple{Int64, Int64, Float64}

This inconsistency is vexxing.

It’s because tuples are a very special type in Julia: they are covariant whereas all other types are invariant. This arises from their use to specify method signatures.

Note that you can use typed globals with named tuples like this:

x::NamedTuple{(:a, :b), Tuple{Number, Int}} = (a=1, b=2.0)

or the shorter

x::@NamedTuple{a::Number, b::Int} = (a=1, b=2.0)

but I suppose that the syntax you propose (which is not about typed globals) could also be made to work…

3 Likes

It’s worth mention that x::T=val isn’t restricted to globals:

let x::Int = 5
    x = 6.5 # error
end

I would think NamedTuples, which are invariant, are used for specifying keyword argument method signatures?

The types of keyword arguments are not really part of method signatures, just the names:

julia> foo(; a::Int) = 1
foo (generic function with 1 method)

julia> foo(x::Int; b::Int) = 2
foo (generic function with 2 methods)

julia> methods(foo)
# 2 methods for generic function "foo":
[1] foo(; a) in Main at REPL[1]:1
[2] foo(x::Int64; b) in Main at REPL[2]:1

Most of the time I wouldn’t bother annotating the types of keyword arguments. The rule of thumb is argument annotations should only be used for dispatch, not for documentation or type safety. (And keyword arguments do not enter into dispatch.)

3 Likes

And yet the keyword argument types are respected, despite their types not being reported by methods:

julia> foo(; a::Number=1) = a
foo (generic function with 1 method)

julia> methods(foo)
# 1 method for generic function "foo":
[1] foo(; a) in Main at REPL[1]:1

julia> foo()
1

julia> foo(a=π)
π = 3.1415926535897...

julia> foo(a=1+1im)
1 + 1im

julia> foo(a="Hello, world!")
ERROR: TypeError: in keyword argument a, expected Number, got a value of type String

However methods that specialize on keyword arguments (by overloading keyword arguments) is disallowed.