Hi, I defined some type UNTuple as union of NTuple and NamedTuple{Name,NTuple{N,...}} and got the following unexpected behavior. What I hope for the UNTuple is an N-tuple of numbers of same type. The behavior seems strange and unexpected:

julia> const NNTuple{N,T,NM} = NamedTuple{NM, NTuple{N,T}} where {N,T<:Number,NM}
NamedTuple{NM, Tuple{Vararg{T, N}}} where {N, T<:Number, NM}
julia> const UNTuple{N,T<:Number} = Union{NTuple{N,T},NNTuple{N,T}}
Union{Tuple{Vararg{T, N}}, NamedTuple{NM, Tuple{Vararg{T, N}}} where NM} where {N, T<:Number}
julia> (1,2.0) isa UNTuple{2}
true
julia> (1,2.0) isa NTuple{2}
false
julia> (1,2.0) isa NNTuple{2}
false
julia> (1,2.0) isa Union{NTuple{2},NNTuple{2}}
false
julia> (1,2.0) isa Union{NTuple{N},NNTuple{N}} where N
false

So whatâ€™s going on here and how to define UNTuple correctly?

julia> let T=Union{NTuple{N,T}, NamedTuple{K,NTuple{N,T}} where K} where {N,T<:Number}
Tuple{Int,Int} <: T,
Tuple{Int,Float64} <: T, # should be false
NamedTuple{(:a,:b),Tuple{Int,Int}} <: T,
NamedTuple{(:a,:b),Tuple{Int,Float64}} <: T
end
(true, true, true, false)

This is how it should behave:

julia> let T=Union{NTuple{N,T} where {N,T<:Number}, NamedTuple{K,NTuple{N,T}} where {K,N,T<:Number}}
Tuple{Int,Int} <: T,
Tuple{Int,Float64} <: T,
NamedTuple{(:a,:b),Tuple{Int,Int}} <: T,
NamedTuple{(:a,:b),Tuple{Int,Float64}} <: T
end
(true, false, true, false)

It seems that NTuple{N,T} is defined such that T cannot be Union{Int,Float64} unless explicitly forced. So my question would be: how to force T<:Number to be a concrete numerical type?

Iâ€™ll just give my two cents to see if I got the full picture myself. Maybe it can help anyone else too (or clear up my misconceptions!).

I think what happens in that last example is that NTuple{2,T} where T<:Number represents Tuple{T,T} where T <: Number. The type of any object which will be checked against that tuple type has to be a concrete type. Hence any instance of Tuple{Int, Float64} will have exactly that type. Since Int and Float64 are not the same T, we have that Tuple{Int,Float64} <: NTuple{2,<:Number} is false. Note that T<:Numberis forced to be a concrete, numerical type. Itâ€™s just that it has to be the same concrete type in both entries of the tuple.

That NTuple{2,Union{Int,Float64}} works is due to the covariance of Tuple types, as @simsurace pointet out. Note that Union{Int,Float64} is not a concrete type, so Tuple{Int,Float64} <: NTuple{2,Union{Int,Float64}} is true (but there can be no instance of that type on the right).

Neither NTuple{2,Union{Int,Float64}} <: NTuple{2,<:Number} nor NTuple{2,<:Number} <: NTuple{2,Union{Int,Float64}} are true since there are tuples which will fall into one type, but not into the other.

When thinking about types, I found this video pretty helpful which was posted somewhere else on this discourse. Types and subtyping essentially works like set and subset calculations in mathematics. A type union is basically a union set.

Yes. Hence, why this seems like a bug. Itâ€™s some sort of interaction between covariant and invariant types.

To illustrate, this works correctly:

julia> let T = Union{Tuple{T,T}, Tuple{T,T,T}} where T<:Number
@show Tuple{Int,Int} <: T
@show Tuple{Int,Float64} <: T
@show Tuple{Int,Int,Int} <: T
@show Tuple{Int,Int,Float64} <: T
end;
Tuple{Int, Int} <: T = true
Tuple{Int, Float64} <: T = false
Tuple{Int, Int, Int} <: T = true
Tuple{Int, Int, Float64} <: T = false

but this doesnâ€™t:

julia> struct Foo{X,Y} end
let T = Union{Tuple{T,T}, Tuple{T,T,T}, Foo{T,T}} where T<:Number
@show Tuple{Int,Int} <: T
@show Tuple{Int,Float64} <: T
@show Tuple{Int,Int,Int} <: T
@show Tuple{Int,Int,Float64} <: T
@show Foo{Int,Int} <: T
@show Foo{Int,Float64} <: T
end;
Tuple{Int, Int} <: T = true
Tuple{Int, Float64} <: T = true
Tuple{Int, Int, Int} <: T = true
Tuple{Int, Int, Float64} <: T = true
Foo{Int, Int} <: T = true
Foo{Int, Float64} <: T = false

Yes, thatâ€™s what I meant, I think my wording wasnâ€™t right. I just tried to think of the subsetting as â€ścan I find an instance that would fall into one type, but not the otherâ€ť.

Ah, now I see. Thanks for spelling it out! It does seem pretty weird â€¦

Not sure itâ€™s a bug though. In the section on diagonal types and equality constraints of the manual you posted, it says

f(a::Array{T}, x::T, y::T) where {T} = ...

In this case, T occurs in invariant position inside Array{T}. That means whatever type of array is passed unambiguously determines the value of T â€“ we say T has an equality constraint on it. Therefore in this case the diagonal rule is not really necessary, since the array determines T and we can then allow x and y to be of any subtypes of T. So variables that occur in invariant position are never considered diagonal. This choice of behavior is slightly controversial â€“ some feel this definition should be written as

Consider this example which should match our discussion:

julia> Tuple{Number, Number} <: Tuple{T, T} where T <: Number
false
julia> Tuple{Number, Number} <: Union{Tuple{T, T}, Vector{T}} where T <: Number
true

As soon as we pack a parametric type in the set which allows us to identify T exactly (even as an abstract type), i.e. not a tuple type, the constraint on the tuple types having diagonal parameters is lifted.

In this example we have then

julia> Tuple{Number, Number} <: (Union{Tuple{T,T}, Vector{T}} where T<:Number)
true
julia> Tuple{Number, Number} <: (Union{Tuple{T,T}, Vector} where T<:Number)
false

but also

julia> Vector{String} <: (Union{Tuple{T,T}, Vector{T}} where T<:Number)
false
julia> Vector{String} <: (Union{Tuple{T,T}, Vector} where T<:Number)
true

so they are not the same set (EDIT: nor subsets of each other) and I think it works as intended (according to the manual).
But itâ€™s definitely a pretty subtle point!

The point that this misses, however, is that although this behavior is appropriate when the outside container is a Tuple, itâ€™s not appropriate when the outside container is a Union.

Namely, as part of a Tuple, the invariant type must be packed in alongside the covariant Tuple types, and therefore can be expected to specify their type parameter. But as part of a Union, the invariant type may be missing (as is the case here), in which case it does no good to expect it to specify the parametric type for the other covariant typesâ€”how could it?

I see your point (that the discussion in the docs is focused on tuple types, not on union types). And as I said, Iâ€™m not entirely sure if this is intended or not (but more and more, based on the discussion below).

What exactly is the behavior you would expect?

I donâ€™t get this. When we put the type parameter in the union and use the same parameter in all places, they better all be the same types over which we take the union. If we put no parameter that occurs in a tuple also in a invariant position (like Union{Tuple{T, T}, Vector}) within the union, the tuples are still restricted to the diagonal types and nothing unexpected happens.

Reading this type union

Union{Tuple{T,T}, Vector{T}} where T <: Number

I would always expect that T goes over all types which are subtypes of Number, and that there can be no T that appears in some of the Vector, but not in the Tuple and vice versa.

The only question in my mind is whether abstract types are included in the union or not.

Tuple{T,T} might suggest that only concrete types T <: Number are included, because in isolation, Tuple{T,T} where T<:Number allows only concrete types

but Vector{T} where T<:Number doesnâ€™t exclude any abstract types for T in any other context (that I can think of right now)

I guess the question is, which behavior should take precedence over the other (the covariant parameters in the tuple types or the invariant ones). Reading the docs, it sounds to me like restricting the parameters for the tuples is treated as the exception, since it is required for matching function signatures in a meaningful way. But apart from that, the type parameters are always(?) invariant and unions range over all possible types of the parameter.

And more importantly, a (perhaps the?) basic property of unions should be that they grow as we add things.

With the current behavior

Vector{Number} <: Union{Tuple{T,T}, Vector{T}} where T <: Number

is true, which is what I would expect, since in any other situation Vector{T} where T <: Number allows abstract types for T. If adding Vector{T} to the union should enforce that T iterates over all subtypes of Number and is replaced in all three places where it occurs without changing the behavior of the tuple types then Vector{Number} <: Union{Tuple{T,T}, Vector{T}} where T <: Number should be false.

I think this would also imply that @jinfreedomâ€™s example relation

(Union{Tuple{T,T}, Vector{T}} where T <: Number) <: (Union{Tuple{T,T}, Vector} where T <: Number)

would be true. This looks reasonable to me, but at the same time, the following would then be false (e.g. Number would be contained in the left, but not in the right):

(Union{T, Vector{T}} where T <: Number) <: (Union{T, Vector{T}, Tuple{T, T}} where T <: Number)

Should adding a Tuple to an arbitrary type union make the whole union smaller than just leaving it away?

The only other option I see would be that the tuple types are treated as they behave in isolation (only including concrete/diagonal types) and the vector type as well (including also abstract types), but that is just

Union{Tuple{T,T}, Vector{S}} where {S<:Number,T <: Number}

tl;dr If T should be the same T in all places in the union, we should rather â€świdenâ€ť the tuple types instead of restricting the other types to ensure (semantically) that unions grow when things are added.

Simple. I would expect that all instances of T are the same exact type (and therefore, T cannot be an abstract type if itâ€™s a Tuple type parameter). This means I agree with this part of the devdocs:

slightly controversial â€“ some feel this definition should be written as

f(a::Array{T}, x::S, y::S) where {T, S<:T} = ...

I didnâ€™t hold this view beforeâ€”I had thought the current behavior was okâ€”but I changed my mind.

Examples:

This should be (true, false, true, false, false, true, true):

julia> let T=Union{Tuple{T,T}, Vector{T}} where T <: Number
Tuple{Int,Int} <: T,
Tuple{Int,Float64} <: T,
Union{Tuple{Int,Int}, Vector{Int}} <: T,
Union{Tuple{Int,Float64}, Vector{Int}} <: T,
Union{Tuple{Int,Float64}, Vector{Number}} <: T,
Vector{Int} <: T,
Vector{Number} <: T
end
(true, true, true, true, true, true, true)

I previously believed it was ok for the 5th item should be true, but I changed my mind.

Meanwhile, this should be (true, true):

julia> let T=Union{Tuple{T,T}, Vector} where T <: Number
(Union{Tuple{T,T}, Vector{T}} where T <: Number) <: T
end,
let T=Union{T, Vector{T}, Tuple{T, T}} where T <: Number
(Union{T, Vector{T}} where T <: Number) <: T
end
(false, true)

The reason I changed my mind, is because I needed to in order for that false to be true. Once you make that decision, to be consistent and demand that T refer to the exact same type everywhere that T is valid, then all of these problems get cleaned up simultaneously, and the OP question would not have occurred.

If adding Tuple to the union restricts the parameter to being concrete, then this would turn into (true, false). The way to make both true would be to restrict types parameters always to range over concrete types.

In the current behavior and in your suggestion, adding one type will dictate what happens to the others in the union â€“ personally, I think allowing abstract types in the tuple when the parameter appears also in an invariant position is the more sensible choice.