Strange behavior of arrays of UnionAll type

Though I thought I understood UnionAll types pretty well by now, some behavior of them are still hard to understand.

Consider the following example:

julia> struct MyType{N,S<:NTuple{N}}
           t::S
       end

MyType has two type parameters N and S, where the latter depends on the former. I create instances of two different MyType types—one with an Int64 tuple and another with a Float64 tuple—and store them in a vector:

julia> m1 = MyType{3,NTuple{3,Int64}}((1,2,3))
MyType{3,Tuple{Int64,Int64,Int64}}((1, 2, 3))

julia> m2 = MyType{3,NTuple{3,Float64}}((1.0,2.0,3.0))
MyType{3,Tuple{Float64,Float64,Float64}}((1.0, 2.0, 3.0))

julia> v = [m1,m2]
2-element Array{MyType{3,S} where S<:(Tuple{Vararg{T,N}} where T) where N,1}:
 MyType{3,Tuple{Int64,Int64,Int64}}((1, 2, 3))
 MyType{3,Tuple{Float64,Float64,Float64}}((1.0, 2.0, 3.0))

And here goes some behavior that I don’t understand: why does the following generate an error:

julia> v::Vector{<:MyType{3}}
ERROR: TypeError: typeassert: expected Array{#s1,1} where #s1<:(MyType{3,S} where S<:(Tuple{T,T,T} where T)), got Array{MyType{3,S} where S<:(Tuple{Vararg{T,N}} where T) where N,1}

even though the following does not?

julia> v::Vector{<:(MyType{3,S} where {S})}
2-element Array{MyType{3,S} where S<:(Tuple{Vararg{T,N}} where T) where N,1}:
 MyType{3,Tuple{Int64,Int64,Int64}}((1, 2, 3))
 MyType{3,Tuple{Float64,Float64,Float64}}((1.0, 2.0, 3.0))

I find this strange because MyType{3} in the failing example is also a UnionAll type that is not much different from MyType{3,S} where {S} in the succeeding example:

julia> MyType{3}
MyType{3,S} where S<:(Tuple{T,T,T} where T)

Another strange behavior. v is typed as

julia> typeof(v)
Array{MyType{3,S} where S<:(Tuple{Vararg{T,N}} where T) where N,1}

where the type parameter N is not specified. However, because both entries of v are MyType with length-3 tuples, I thought we should be able to fix N to 3 in the above type, but that was not the case:

julia> v::Array{MyType{3,S} where S<:(Tuple{Vararg{T,3}} where T),1}
ERROR: TypeError: typeassert: expected Array{MyType{3,S} where S<:(Tuple{Vararg{T,3}} where T),1}, got Array{MyType{3,S} where S<:(Tuple{Vararg{T,N}} where T) where N,1}

I wonder if somebody could explain these behaviors.

The main reason for asking this question: I had a custom type similar to MyType, and wanted to create a function taking a vector similar to the above v, but specifying the type of the function’s argument as ::AbstractVector{<:MyType{3}} did not work because of the phenomenon described above.

1 Like

After a couple of rounds of experimenting, I think I can raise one more question and then answer the questions above.

julia> typeof([m1,m2])
Array{MyType{3,S} where S<:(Tuple{Vararg{T,N}} where T) where N,1}

julia> Vector{<:MyType{3}}
Array{#s5,1} where #s5<:(MyType{3,S} where S<:(Tuple{T,T,T} where T))

julia> Vector{<:(MyType{3,S} where {S})}
Array{#s6,1} where #s6<:(MyType{3,S} where S)

The first surprising bit here is that when packaging m1 and m2 together in a vector, the information about the tuple size N is lost in the triangular type parameters but not in the top level parameters. So my question is, why is this happening?

Now given this unfortunate event, let’s try and hack things around. If v is defined as:

v = (MyType{3,S} where {T, S<:NTuple{3,T}})[m1,m2]

thus forcefully preserving the inner tuple size parameter, things work out as expected.

julia> v = (MyType{3,S} where {T, S<:NTuple{3,T}})[m1,m2]
2-element Array{MyType{3,S} where S<:(Tuple{T,T,T} where T),1}:
 MyType{3,Tuple{Int64,Int64,Int64}}((1, 2, 3))
 MyType{3,Tuple{Float64,Float64,Float64}}((1.0, 2.0, 3.0))

julia> v::Vector{<:MyType{3}}
2-element Array{MyType{3,S} where S<:(Tuple{T,T,T} where T),1}:
 MyType{3,Tuple{Int64,Int64,Int64}}((1, 2, 3))
 MyType{3,Tuple{Float64,Float64,Float64}}((1.0, 2.0, 3.0))

julia> v::Vector{<:(MyType{3,S} where {S})}
2-element Array{MyType{3,S} where S<:(Tuple{T,T,T} where T),1}:
 MyType{3,Tuple{Int64,Int64,Int64}}((1, 2, 3))
 MyType{3,Tuple{Float64,Float64,Float64}}((1.0, 2.0, 3.0))

Notice that Vector{<:MyType{3}} is short for Array{#s5,1} where #s5<:(MyType{3,S} where S<:(Tuple{T,T,T} where T)), that is you are only allowed to choose #s5 from the subtypes of (MyType{3,S} where S<:(Tuple{T,T,T} where T)) where the tuple size is properly inferred. However, Vector{<:(MyType{3,S} where {S})} is short for Array{#s6,1} where #s6<:(MyType{3,S} where S) , so #s6 is now free to be any subtype of (MyType{3,S} where S) . In other words, because [m1,m2] has a less specialized element type than the assertion type, it is not passing the assertion test. This is just a more complicated example of:

julia> Real[1,2]::Vector{<:Integer}
ERROR: TypeError: typeassert: expected Array{#s1,1} where #s1<:Integer, got Array{Real,1}

julia> Integer[1,2]::Vector{<:Real}
2-element Array{Integer,1}:
 1
 2

So it seems the take home lesson (or bug) here is that typejoining doesn’t work as expected for parametric types when constructing arrays of custom types.

julia> typeof(m1)
MyType{3,Tuple{Int64,Int64,Int64}}

julia> typeof(m2)
MyType{3,Tuple{Float64,Float64,Float64}}

julia> typejoin(typeof(m1), typeof(m2))
MyType{3,S} where S<:(Tuple{Vararg{T,N}} where T) where N

I don’t know if this is a bug, but it should warrant further discussion in an issue if one does not exist already.

1 Like

Thanks for the inputs! An issue about this problem is created here.