Don't understand behaviour of NamedTuple{...,Tuple{...,T}} where T

Here’s an example to enter in the REPL. I don’t understand the behaviour of Z(x) below.
I’m using Julia 1.4.2.

julia> Y = NamedTuple{(:a,:b), Tuple{String,Real}}
julia> Z = NamedTuple{(:a,:b), Tuple{String,T}} where T <: Real
julia> f(y::Y) = y
julia> g(z::Z) = z

julia> x = (a = "abc", b = 1)

julia> f(x) # MethodError as expected
julia> g(x) # produces x as expected

julia> Y(x) # expected behaviour
NamedTuple{(:a, :b),Tuple{String,Real}}(("abc", 1))

julia> Z(x) # I don't understand this error - "T not defined"
ERROR: UndefVarError: T not defined
Stacktrace:
 [1] NamedTuple{(:a, :b),Tuple{String,T}} where T<:Real(::NamedTuple{(:a, :b),Tuple{String,Int64}}) at ./namedtuple.jl:85
 [2] top-level scope at REPL[9]:1
 [3] eval(::Module, ::Any) at ./boot.jl:331
 [4] eval_user_input(::Any, ::REPL.REPLBackend) at /buildworker/worker/package_linux64/build/usr/share/julia/stdlib/v1.4/REPL/src/REPL.jl:86
 [5] run_backend(::REPL.REPLBackend) at /home/graham/.julia/packages/Revise/ucYAZ/src/Revise.jl:1184
 [6] top-level scope at none:0
 [7] eval(::Module, ::Any) at ./boot.jl:331
 [8] eval_user_input(::Any, ::REPL.REPLBackend) at /buildworker/worker/package_linux64/build/usr/share/julia/stdlib/v1.4/REPL/src/REPL.jl:86
 [9] run_backend(::REPL.REPLBackend) at /home/graham/.julia/packages/Revise/ucYAZ/src/Revise.jl:1184
 [10] top-level scope at none:0

Why is “T not defined”?

I’m trying to create a single type alias that I can use both in the context of method signatures and as a constructor of NamedTuples. I thought Z above was the way to go. Variations on Z don’t work or are equal to Y:

julia> NamedTuple{(:a,:b), Tuple{String,T} where T <: Real}
ERROR: NamedTuple field type must be a tuple type

julia> NamedTuple{(:a,:b), Tuple{String,T where T <: Real}} == Y
true

julia> NamedTuple{(:a,:b), Tuple{String,<: Real}}
ERROR: NamedTuple field type must be a tuple type

I may be wrong, but I think your problem is that:

julia> Z = NamedTuple{(:a,:b), Tuple{String,T}} where T <: Real
NamedTuple{(:a, :b),Tuple{String,T}} where T<:Real

julia> isconcretetype(Z)

In other words, Z is a abstract type, it serves the purpose of dynamic dispatch (so you can use it as the type of a parameter and it works as expected) but you cannot create an instance of it. I am actually surprised that Z(x) did not give a better error message, saying there was no constructor for the abstract type.

OK thanks Henrique, that explanation sounds right to me, even though the UndefVarError is confusing.

Here’s a followup question: why is Y a concrete type but both Tuple{String,Real} and

struct ABC
    a::String
    b::Real
end

are not? What is it about wrapping Tuple{String,Real} in a NamedTuple that makes it concrete?

I think believing Y was abstract initially led me to believe I could also construct a value of type Z.

uhh…

julia> struct ABC
           a::String
           b::Real
       end

julia> isconcretetype(ABC)
true

I am not sure if the Tuple, however, is not kinda of special. Nor tuple.jl, nor ntuple.jl (both in the Julia source code) seem to really define the struct. Also, by my intuition these two should not be different:

julia> isconcretetype(Tuple{String,Real})
false

julia> struct MyTuple{A, B}; a :: A; b :: B; end

julia> isconcretetype(MyTuple{String, Real})
true

So there is nothing to do with NamedTuple actually, Tuple that seems to be the strange one.

I have missed this section of the manual which says:

However, there are three key differences:
[…]
2. Tuple types are covariant in their parameters: Tuple{Int} is a subtype of Tuple{Any} . Therefore Tuple{Any} is considered an abstract type, and tuple types are only concrete if their parameters are.

So the Tuple type really is special. Different from other types that may have abstract type fields, Tuples cannot do so.

1 Like

Great, thanks again. I realise what I did with isconcretetype: for ABC I mistakenly tested it on a value of type ABC rather than on the type ABC itself - isconcretetype then returns false, presumably because if the argument is not a type at all, it certainly can’t be a concrete type.