# Strange behavior of union of types

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?

Seems like a bug.

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

This seems to arise from the following behavior (that I donâ€™t fully understand):

``````julia> Tuple{Int,Float64} <: NTuple{2,Number}
true

julia> Tuple{Int,Float64} <: NTuple{2}
false

julia> NTuple{2,Number} <: NTuple{2}
false

julia> NTuple{2,Number} <: NTuple{2,Any}
true
``````
1 Like

Okay, I think that behavior makes sense.

``````julia> NTuple{2,Number}, NTuple{2,T} where T<:Number
(Tuple{Number, Number}, Tuple{T, T} where T<:Number)

julia> @show Tuple{Int,Float64} <: NTuple{2,  Number}
@show Tuple{Int,Float64} <: NTuple{2,<:Number};
Tuple{Int, Float64} <: NTuple{2, Number} = true
Tuple{Int, Float64} <: NTuple{2, <:Number} = false

julia> @show NTuple{2,   Number} <: NTuple{2,   Any}
@show NTuple{2,   Number} <: NTuple{2, <:Any}
@show NTuple{2, <:Number} <: NTuple{2,   Any}
@show NTuple{2, <:Number} <: NTuple{2, <:Any};
NTuple{2, Number} <: NTuple{2, Any} = true
NTuple{2, Number} <: NTuple{2, <:Any} = false
NTuple{2, <:Number} <: NTuple{2, Any} = true
NTuple{2, <:Number} <: NTuple{2, <:Any} = true
``````

Entertainingly, the `show` method prints the anonymous variable name:

``````julia> NTuple{2,<:Number}
Tuple{var"#s1", var"#s1"} where var"#s1"<:Number
``````

Seems unrelated to the OP though.

Iâ€™m still a bit confused:

``````julia> Number<:Number
true

julia> NTuple{2,Number} <: NTuple{2,<:Number}
false

julia> NTuple{2,<:Number}
Tuple{var"#s52", var"#s52"} where var"#s52"<:Number
``````

Does this help?

``````julia> Tuple{Number,Number} <: Tuple{T,T} where T<:Number
false

julia> Tuple{Number,Number} >: Tuple{T,T} where T<:Number
true
``````
1 Like

Oh, I see, thanks!

OK, here seems to be some hint: the parameter `T<:Number` can not only be a numerical type but also be a union of numerical typesďĽš

``````julia> const UNTuple{N,T} = Union{NTuple{N,T},NamedTuple{M,NTuple{N,T}} where M} where T<:Number
Union{Tuple{Vararg{T, N}}, NamedTuple{M, Tuple{Vararg{T, N}}} where M} where {N, T<:Number}

julia> Tuple{Int,Float64} <: UNTuple{2}
true

julia> Tuple{Int,Float64} <: UNTuple{2,T} where T<:Number
true

julia> Tuple{Int,Float64} <: UNTuple{2,Union{Int,Float64}}
true

julia> Union{Int,Float64}<:Number
true
``````

However, this is not the case for `NTuple`:

``````julia> Tuple{Int,Float64} <: NTuple{2,T} where T<:Number
false

julia> Tuple{Int,Float64} <: NTuple{2,Union{Int,Float64}}
true
``````

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?

See Types Â· The Julia Language, in contrast to other types, tuple types are covariant. EDIT: maybe unrelated, but still good to remember.

3 Likes

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<:Number` is 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.

2 Likes

Thank you all for your input. I still get confused by the following very simple example

``````julia> Tuple{Int,Float64}<:Union{Tuple{T,T}, Vector{T}} where T<:Number
true

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

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

which does not make any sense to me, as the first union should be a subset of the second union, shouldnâ€™t it?

1 Like

It has to be the same type, but not necessarily concrete:

``````julia> (NTuple{N,T} where {N,T<:Real}) <: NTuple{N,T} where {N,T<:Number}
true
``````

The comparison of concrete types happens naturally when you call `isa` on an instance, which is of course concrete.

It does seem related to this though: Diagonal Types - More about types Â· The Julia Language.

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
``````
3 Likes

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!

2 Likes

This is THE point. Thanks @Sevi !

1 Like

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?

Thus, I maintain that this seems like a bug.

1 Like

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.

``````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.