Union type confusion

Consider an abstract type with a parameter T and its two concrete types:

abstract type AbstractMyType{T} end
struct MyType1{T} <: AbstractMyType{T} end
struct MyType2{T} <: AbstractMyType{T} end

Construct a vector of instances of the two concrete types with the same parameter T = Int:

julia> v = [MyType1{Int}(), MyType2{Int}()]
2-element Vector{AbstractMyType{Int64}}:
 MyType1{Int64}()
 MyType2{Int64}()

The result is a vector of the abstract type.

Now, here is a thing that I don’t understand:

julia> v isa AbstractVector{AbstractMyType{T}} where {T<:Number}
true

julia> v isa AbstractVector{AbstractMyType{<:Number}}
false

I thought AbstractMyType{<:Number} was a shorthand of AbstractMyType{T} where {T<:Number}. Why do they produce the different results?

It is:

julia> v isa AbstractVector{AbstractMyType{T} where T <: Number}
false

The point is that

AbstractVector{AbstractMyType{T} where T <: Number}

is not the same as

AbstractVector{AbstractMyType{T}} where T <: Number

The second one subsumes all AbstractVectors whose element type is some type of the form AbstractMyType{T} where T is a subtype of Number. The first one applies only to AbstractVectors whose element type is exactly AbstractMyType{<:Number}.

4 Likes

Could you give an example vector of this type? Not sure how to construct a vector whose element type is exactly AbstractMyType{<:Number}.

julia> v = AbstractMyType{<:Number}[ MyType1{Int}(), MyType2{Number}(), MyType2{Real}() ]
3-element Vector{AbstractMyType{<:Number}}:
 MyType1{Int64}()
 MyType2{Number}()
 MyType2{Real}()

I don’t think Julia’s array literals will divine out such an eltype, but you can explicitly request it:

julia> AbstractMyType{<:Number}[MyType2{Int}(), MyType1{Float64}()]
2-element Vector{AbstractMyType{<:Number}}:
 MyType2{Int64}()
 MyType1{Float64}()

But more to your original point, you can add another where clause to say that it can match any subtype of AbstractMyType as long as its parameter is <: Number.

julia> v = [MyType1{Int}(), MyType2{Int}()];

julia> v isa AbstractVector{<:AbstractMyType{<:Number}}
true
3 Likes

A good point, but I would like to note that this also includes types like Vector{MyType{Int}} whose eltype is the concrete MyType{Int}, whereas AbstractVector{AbstractMyType{T}} where {T<:Number} excludes such types. Therefore, the two type specifications are different.

There are cases where this difference matters. For example, I am actually trying to specify the type of v in a function signature as

function myfun(v::AbstractVector{AbstractMyType{T}}) where {T<:Number}

in order to make sure that the eltype of v is always the abstract AbstractMyType, , because myfun is expected to take aways a vector of abstract-type elements.

Why would you want to do that? Coming from generic programming a function taking a vector of AbstractMyTypes could only do some reasonable things with its inputs, such as iterating over the vector and calling some methods on some elements – which would need to work for AbstractMyTypes. In any case, the function should probably also work with vectors holding some concrete subtype such as MyType1.
In OOP this idea is known as the Liskov substitution principle and the related quote by Jon Postel:

be conservative in what you do, be liberal in what you accept from others

1 Like