# 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 `typejoin`ing 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