# Subtypes of concrete types in Julia

# Introduction

Today my post is about subtypes of concrete types. It is mostly academic, but I hope it will be useful for readers wanting to get a better understanding of corner cases Julia’s type system.

The post was written under Julia 1.8.0 (with special thanks for all people who contributed to this release!).

# What is a concrete type in Julia?

In Julia a type is concrete it can have a direct instance, that is, some type
`T`

is concrete if there exists at least one value `v`

such that
`typeof(v) === T`

.
For every type you can check whether it is concrete using the
`isconcretetype`

function.

Today I want to discuss the following sentence from the section on Types from the Julia Manual in relation to concrete types:

One particularly distinctive feature of Julia’s type system is that concrete types may not subtype each other: all concrete types are final and may only have abstract types as their supertypes.

From this sentence some readers conclude that concrete types cannot have subtypes. However, it is not the case. Concrete types in Julia can have subtypes as long as these subtypes are not concrete.

You might ask does this ever happen in practice? The answer is that it happens and here are the examples when it does.

The first one is `Union{}`

type. This type is not concrete and has no values.
However, it is a subtype of all types, including concrete types, for example:

```
julia> Union{} <: Int
true
julia> Union{} <: Vector{Missing}
true
```

The other case is `Type{T}`

type, where `T`

is a `DataType`

(i.e. if `T`

has
type `DataType`

). All common concrete types are subtypes of `DataType`

, e.g.
integers or vectors. So types like `Int`

or `Vector{Missing}`

have `DataType`

type:

```
julia> typeof(Int)
DataType
julia> typeof(Vector{Missing})
DataType
```

which means that `DataType`

must be concrete, and indeed it is:

```
julia> isconcretetype(DataType)
true
```

Although `DataType`

is concrete, it has `Type{Int}`

and `Type{Vector{Missing}}`

as its subtypes (and these types must not, and are not, concrete as we
discussed above):

```
julia> Type{Int} <: DataType
true
julia> Type{Vector{Missing}} <: DataType
true
julia> isconcretetype(Type{Int})
false
julia> isconcretetype(Type{Vector{Int}})
false
```

# Why these subtyping considerations matter?

The most important lesson learned here is that in your code you should not assume that concrete type cannot have subtypes, as it can (although these subtypes cannot be concrete themselves). This observation is mostly relevant for package developers, who need to write generic code.

However, there are some practical situations when one can be affected by these subtyping rules. The most common is when one is working with missing values.

Assume that I generate some random matrix containing either `1`

or `missing`

:

```
julia> using Random
julia> Random.seed!(1234);
julia> mat = rand([1, missing], 10, 3)
10×3 Matrix{Union{Missing, Int64}}:
1 missing 1
missing missing 1
1 1 missing
missing 1 1
1 1 missing
1 missing 1
missing missing 1
missing missing missing
1 missing missing
missing missing missing
```

Now, I want to compute the sums of its rows, while skipping missing values. Here is how you can do it:

```
julia> [sum(skipmissing(row)) for row in eachrow(mat)]
10-element Vector{Int64}:
2
1
2
2
2
2
1
0
1
0
```

However, a very similar codes that follow do not work:

```
julia> [sum(skipmissing(identity.(row))) for row in eachrow(mat)]
ERROR: ArgumentError: reducing with add_sum over an empty collection of element type Union{} is not allowed.
julia> [sum(skipmissing([x for x in row])) for row in eachrow(mat)]
ERROR: ArgumentError: reducing with add_sum over an empty collection of element type Union{} is not allowed.
```

What is the reason for the difference? In the `skipmissing(row)`

case `row`

is a view and retains information about element type of the whole array, which
is `Union{Missing, Int64}`

, so it is able to properly compute sum even in the
case when all values in a row are missing.

On the other hand both `identity.(row)`

and `[x for x in row]`

materialize the
row and perform type narrowing. This type narrowing means that in rows that
only contain `missing`

values the information about `Int64`

is lost and we get
an error. Let us see it step by step:

```
julia> row = last(eachrow(mat))
3-element view(::Matrix{Union{Missing, Int64}}, 10, :) with eltype Union{Missing, Int64}:
missing
missing
missing
julia> x = identity.(row)
3-element Vector{Missing}:
missing
missing
missing
julia> eltype(skipmissing(x))
Union{}
```

As you can see, since `skipmissing`

strips the `Missing`

part from the source
vector element type, we are left with `Union{}`

.

Unfortunately, such errors happen from time to time when one works with
data having missing values. For such cases in Julia many (but not all) common
reduction functions support the `init`

keyword, so you can do:

```
julia> [sum(skipmissing(identity.(row)), init=0) for row in eachrow(mat)]
10-element Vector{Int64}:
2
1
2
2
2
2
1
0
1
0
```

and all is good even if type inference produces `Union{}`

.

# Conclusions

The post today was less practical than usual. However, I hope you will find it useful when Julia tries to take you into a deep dark type system forest where 2+2=5, and the path leading out is only wide enough for one (Mikhail Tal).