Type annotations with abstract types (or when is `<:` required?)

Let’s assume I define a function f with two parameters x and n, where x should be a vector of integer numbers and n should be an integer. I though that I could use the abstract Integer type to write the following function header:

function f(x::Vector{Integer}, n::Integer)
    x .* n
end

However, this gives an error when calling f([1, 2, 3], 2):

ERROR: MethodError: no method matching f(::Vector{Int64}, ::Int64)
Closest candidates are:
  f(::Vector{Integer}, ::Integer)

When I change the type parameter of Vector to <:Integer, everything works as expected, i.e.

function f(x::Vector{<:Integer}, n::Integer)
    x .* n
end

Intuitively, I expected the abstract type Integer to work, because by definition it should accept all its (direct and indirect) subtypes. This is the case for n::Integer, but not x::Vector{Integer}. What is the reasoning behind this behavior? Where is the error in my thinking that it should work?

This is described in detail here in the Julia Manual. If something is not clear there can you please comment?

2 Likes

I read that section multiple times, but I don’t think it explains the problem I’m having. It even contains examples like Point{AbstractString}, which conceptually should be Vector{Integer} in my example (both AbstractString and Integer are abstract types) – but this doesn’t work and I need to use Vector{<:Integer}.

I think I’m struggling with how to write type annotations and not types. The UnionAll section even mentions the syntax Array{<:Integer}, but my question is why the <: operator is necessary when Integer is already an abstract type.

A superficial explanation that might be easy to remember is that Integer is an abstract type, so if you input an integer of type Int64, this is a subtype of Integer and is therefore a match:

jl> Int64 <: Integer
true

jl> 3 isa Integer
true

But Vector{Integer} is not abstract type, it is concrete:

jl> isconcretetype(Vector{Integer})
true

Now, [1, 2, 3] has type Vector{Int64}. Since concrete types cannot have subtypes, Vector{Int64} cannot be a subtype of Vector{Integer}, so you don’t get a match:

jl> Vector{Int64} <: Vector{Integer}
false

So why is Vector{Integer} not abstract? Well, it can be instantiated, it’s a vector with heterogeneous elements, like Integer[1, 0x03]. It’s the same thing with Vector{Any}, like Any[5, sin, "hello"]. If Vector{Any} weren’t concrete, you couldn’t make arbitrary collections like this.

This isn’t really the reason for this choice, there are some type-theoretic explanations that are hard to grasp (for me), but my explanation is what I use to remember this.

6 Likes

maybe this helps: Vector{Int} <: Vector{Real} is false??? · JuliaNotes.jl

In short, [1,2,3] is a Vector{Int64}, which cannot contain, for instance, an Int32. Thus it has a constraint that Vector{Integer} does not have. That is why Vector{Int64} cannot be used as a subtype of Vector{Integer}.

1 Like

Note that they use Point{AbstractString} to demonstrate Point{AbstractString} <: Point, and Point{AbstractString} <: Pointy{AbstractString} - which are equivalent here to Vector{Integer} <: Vector and Vector{Integer} <: AbstractArray{Integer} , both of which are true.

The relevant part for this discussion is

julia> Point{Float64} <: Point{Real}
false

Warning

This last point is very important: even though Float64 <: Real we DO NOT have Point{Float64} <: Point{Real} .

It then goes on to explain why this is, and shows an example of how it’s significant for performance reasons.

2 Likes

Thank you, these are all really helpful answers :heart:! I guess I should have read that section more carefully, because as you point out it is already explained.

All three answers by @DNF, @lmiq, and @digital_carver are solutions to my question, so I’m having a hard time marking just one as the solution. I hope this is OK with everyone if I just mark the last answer.

5 Likes