# Simple question about Julia types

Hi!

Given the following statements are true:

``````julia> Matrix <: AbstractMatrix
true

julia> Float64 <: Number
true
``````

why is it that

``````julia> Matrix{Float64} <: AbstractMatrix{Number}
false
``````

?

`Float64` is not `Number`. Itâ€™s a subtype of `Number`.

``````julia> Matrix{Float64} <: AbstractMatrix{Float64}
true

julia> Matrix{Float64} <: AbstractMatrix{<:Number}
true
``````
2 Likes

Thanks, @mkitti, for your quick response!

For more context, there is this part of the manual:

https://docs.julialang.org/en/v1/manual/types/#man-parametric-composite-types

Terms to research this more generally as a topic in computer science is invariant, covariant, and contravariant types: Covariance and contravariance (computer science) - Wikipedia

My superficial understanding is that this subtyping rule is necessary in order to have reasonably fast type inference.

4 Likes

Thanks a lot, @Krastanov, for those specific pointers to the documentation.

Well, thereâ€™s code that works when passed `Matrix{Number}` but fails for `Matrix{Float64}`. So it doesnâ€™t make lots of sense to make `Matrix{Float64} <: Matrix{Number}`.
Simple example of such code: `f(A::Matrix{Number}) = A[1] = big"10"^1000`.

3 Likes

The Julia docs seem rather clear in that the main reason for invariance is mutable types:

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.

Indeed, tuples â€“ which are immutable â€“ are covariant, e.g., `Tuple{Float64} <: Tuple{Number}` is true.

3 Likes

Nothing in those quotes from the docs refers to mutability. If it were possible to restrict subtypes of `Real` to only immutable types (i.e. `struct` instead of `mutable struct`), then the arguments in the docs around practicality would still apply.

One way to understand that `Point{Float64}` is not a subtype of `Point{Real}` is to note that `Point{Real}` is a concrete type, and concrete types cannot be subtyped. Of course, that begs the question, â€śHow come concrete types canâ€™t be subtyped?â€ť Also, that argument does not apply to `AbstractMatrix{Number}`, since `AbstractMatrix{Number}` is not a concrete type.

So, it seems the best conceptual way to understand why `Foo{Float64}` is not a subtype of `Foo{Real}` is the observation pointed out by @aplavin: There are methods that work on `Foo{Real}` but not on `Foo{Float64}`, even when `Foo` is immutable. Hereâ€™s an example:

``````struct Foo{T}
x::T
end

bar(foo::Foo) = typeof(foo)(big(foo.x)^100)
``````
``````julia> bar(Foo{Real}(2))
Foo{Real}(1267650600228229401496703205376)

julia> bar(Foo{Int}(2))
ERROR: InexactError: Int64(1267650600228229401496703205376)
``````

However, Iâ€™m not sure if thatâ€™s the best example, since it relies on introspection of the type of `foo` via `typeof`. Thoughts?

2 Likes

Youâ€™re right, there is no explicit reference to mutability. It was my interpretation/assumption that the memory layout should just play a role if mutating a data structure, i.e., trying to assign an object with `Point{Real}` layout to a place with an `Point{Float64}` layout.

In the end, the variance of data types is a design decision of the language effecting which invariants â€“ in the sense of laws â€“ can be assumed about functions. In particular, in functional languages such as Haskell the types provide a lot of information on what a function can or cannot do:

``````generic :: [a] -> [a]
# Fully generic type, i.e., valid for all `a`
# => Function can at most change the list structure, but not look at elements!

parametric :: Show a => [a] -> String
# Function can assume/call `show` function on elements, but nothing else
# => will also work on all subtypes, i.e., with more specific constraints than `Show`.
``````

Dynamic languages are less stringent and especially with introspection â€“ as in your example â€“ any type assumptions can be broken. Guess this might be another of several reasons why invariance was chosen as the save default in Julia.
Interestingly tuples are an exception, such that `myfun(x::Real, y::AbstractString)` is implicitly considered the same as `yfun(x::T, y::S) where {T<:Real, S<:AbstractString}` saving a bit of thought and some characters of code. As far as I know there is also no alternative syntax, to force invariance in this case. Similarly, with abstract parametric types I would be curious about a real-world example where `myfun(x::AbstractVector{Real})` and maybe further methods are needed instead of the more generic `myfun(x::AbstractVector{T}) where {T <: Real}`.

Hereâ€™s a less introspective example, though itâ€™s more about containers than methods:

``````julia> struct Foo{T}
x::T
end

julia> x = Foo{Real}[Foo(3.0)]
ERROR: MethodError: Cannot `convert` an object of type
Foo{Float64} to an object of type
Foo{Real}
``````

That is, `Foo{Float64}` doesnâ€™t fit into a `Foo{Real}`-typed array.

``````julia> x = Tuple{Real}[(3.0,)]
1-element Vector{Tuple{Real}}:
(3.0,)
``````

I suppose this works because `Tuple{Real}` is an abstract type, so under the hood `Vector{Tuple{Real}}` holds pointers like any other array with abstract eltype.

``````julia> isconcretetype(Tuple{Real})
false

julia> Tuple{Real} == Tuple{<:Real}
true
``````
2 Likes

`Tuple`s are covariant unlike everything else for mostly historical reasons.

2 Likes

I believe itâ€™s mostly because Tuple is used by type system itself (i.e function call argument types are put in a Tuple

3 Likes