Tuple vs Union as struct parameter

This seems OK.

julia> struct TupleTest{T<:Tuple}
       end
julia> TupleTest{Tuple{Int,Float64}}
TupleTest{Tuple{Int64,Float64}}

However this is not:

julia> struct UnionTest{U<:Union}
       end
julia> UnionTest{Union{Int,Float64}}
ERROR: TypeError: in UnionTest, in U, expected U<:Union, got Type{Union{Float64, Int64}}
Stacktrace:
 [1] top-level scope at REPL[5]:1

why is that?

Basically my question boils down to:

julia> Union{Int,Float64} <: Union
false

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

why?

I don’t fully understand the rules here, but

julia> Union{Int,Float64} <: Union{Any}
true

It looks as if the comparison used promote on each union and then applies the <: comparison. For example:

julia> Union{Int,Float64} <: Union{UInt8,Real}
true
julia> Union{Int,Float64} <: Union{UInt8,Float32}
false

I don’t see this in the offical docs though.

Both Tuple and Union are special in Julia and can’t just be thought of as regular parametric types. The weirdness of Tuple is described quite well here, but the section for Union types could probably be improved.

The first thing to note about Union is that there can never be an instance of an object that has a union type as its type. Instances must always have a concrete type, unions just represent a set of types that an object could be, and can also allow the compiler to optimize small unions, for example. As such it simply isn’t possible to dispatch just based on whether something is a union type, since the actual object passed to the function will always have a concrete type, and the function being dispatched on doesn’t know whether your code is type stable or not.

As such, what you are trying to do doesn’t make much sense, since Union is not an abstract type, that Union{Int,Float64} is a subtype of, but instead, Union{Int,Float64} is only a subtype of types, that both Int and Float64 are a subtype of, so e.g. Union{Int,Float64} <: Real == true. If you want to check, whether a type (not an object) is a union type, you could to this with Union{Int,Float64} isa Union, for example, since Union is not the supertype of union types, but union types are actually instances of type Union.

@hendri54 Union types actually don’t rely on promote at all, but instead, when checking whether they are a subtype of something, it is checked, whether every member of the union is a subtype, so it’s easiest to just think of union types as set unions, and <: as checking whether the argument on the left is a subset of the one on the right. Also note that Union{Any} is just equivalent to Any and Union{UInt8,Real} is equivalent to Real, since UInt8 is just a subtype of Real. Order also doesn’t matter for union types.

I hope this hasn’t been too confusing, and it cleared at least some things up. Anybody feel free to correct me, if I glossed over some things.

2 Likes

Very helpful - thank you.

So, if I understand correctly, <: asks: “is any member of the Union on the left guaranteed to be a subtype of any member of the union on the right?” If so, return true; otherwise false.

That makes sense.