Question about Julia type syntax: Why is Array{Int32, 1} <: Array{Integer, 1} false?

Viewed 110

In Julia,

Array{Int32, 1} <: Array{Integer, 1} 

evaluates to false, but

Array{Int32, 1} <: (Array{T, 1} where T <: Integer)

evaluates to true because Int32 <: Integer is true.

In my mind, the first and second expressions communicate the same idea and should evaluate equivalently. Furthermore, the first expression is less cluttered. Is there a reason why Julia's syntax evaluates the former as false but the latter as true? Is there something deep and good about this behavior or is this an oversight in how Arrays/the type system were developed?

3 Answers

See Parametric Composite Types from the manual.

Concrete Point types with different values of T are never subtypes of each other:

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

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}.

In other words, in the parlance of type theory, Julia's type parameters are invariant, rather than being covariant (or even contravariant).

This is for practical reasons: while any instance of Point{Float64} may conceptually be like an instance of Point{Real} as well, the two types have different representations in memory:

  • An instance of Point{Float64} can be represented compactly and efficiently as an immediate pair of 64-bit values;

  • An instance of Point{Real} must be able to hold any pair of instances of Real. Since objects that are instances of Real can be of arbitrary size and structure, in practice an instance of Point{Real} must be represented as a pair of pointers to individually allocated Real objects.

The key difference between Array{Integer, 1} and Array{T, 1} where T <: Integer is that the former is a concrete type, while the later is an abstract type. The reason this makes a difference is that you can make a variable with type Array{Integer, 1}. This is a potentially heterogeneous array, so it has to be implemented as an array of pointers (so slow and heap allocated). With this in mind, it is clear why Array{Int32, 1} <: Array{Integer, 1}=false. If we write a method for the specific type Array{Integer, 1}, that can't be specialized since it is already a concrete type, and we would segfault when we run it on an Array{Int32, 1} which has a completely different data format (inline elements).

There were discussions about making the behaviour you described as default, but they finally reached consensus that the current might be better.

Basically it is a syntax choice of what Vector{Integer} means. There are three types of vairance to choose from:

julia> Vector{Int32} <: (Vector{T} where T <: Integer)
true

julia> Vector{Any} <: (Vector{T} where T >: Integer)
true

julia> Vector{Integer} <: (Vector{T} where {T >: Integer, T <: Integer})
true

The three types of variance correspond to three different operations about the variable. Covariance for read, contravariance for write, and invariance for both. For example in Rust, &'a T is covariant about T, because you can only read from it, while &'a mut T is invariant since it can be both read and written. If ever there being a "write-only" type, contravarance would be a sane default. Given variables in Julia are both read and writable, defaulting to invariance looks a good choice.

Related