Why create an abstract super type in Julia?

What are abstract types?

Abstract types are nodes in the type hierarchy: they group types together. This allows you to write methods that apply to the whole group of types:

julia> abstract type AbstractFoo end

julia> struct Foo1 <: AbstractFoo end

julia> struct Foo2 <: AbstractFoo end

julia> foo_op(x::AbstractFoo) = "yay!"
foo_op (generic function with 1 method)

julia> foo_op(Foo1())
"yay!"

julia> foo_op(Foo2())
"yay!"

Why are abstract types useful?

Abstract types allow you to separate behavior from implementation. This is critical for performance. When you declare an abstract supertype, you automatically inherit the core behavior of the supertype, but are free to implement more efficient implementations of that behavior.

A common example is the AbstractArray abstract type. It represents the ability to access individual elements of some multi-dimensional collection of elements. Given some problem, we can usually choose a subtype of abstract arrays which will yield efficient operations: the additional constraints on a subtype constitute information that the programmer can leverage to make certain operations more efficient.

For example, say we want to find the sum of 1..N. We could use an array of integers, but this would be very inefficient compared to a UnitRange. The choice of UnitRange encodes information about the characteristics of the data; information we can leverage for efficiency. (See this answer for more information on this example).

julia> using BenchmarkTools

julia> @btime sum($(1:1000_000))
  0.012 ns (0 allocations: 0 bytes)
500000500000

julia> @btime sum($(collect(1:1000_000)))
  229.979 μs (0 allocations: 0 bytes)
500000500000

BitArray provides space efficient representations for an array of booleans, SparseArrays provides efficient operations for sparse data, and so on. If you have some data that generally behaves like an abstract array, but has unique characteristics, you can define your own subtype.

This pattern generalizes to other abstract types. Use them to group different implementations of some shared behavior.


A much more practical use case is to create strongly typed, potentially mutually recursive structs. E.g., you could not write the following:

struct Node
    edges::Vector{Edge}
end

struct Edge
    from::Node
    to::Node
end

One way to write this is the rather artificial

abstract type AbstractNode end
abstract type AbstractEdge end

struct Node{E<:AbstractEdge}
    edges::Vector{E}
end

struct Edge{N<:AbstractNode}
    from::N
    to::N
end

Often, with enough experience, this problem will be solved naturally already during the design of a data structure, as in the following:

abstract type Program end
abstract type Expression <: Program end
abstract type Statement <: Program

struct Literal <: Expression
    value::Int
end

struct Var <: Expression
    name::Symbol
end

struct Plus <: Expression
    x::Expression
    y::Expression
end

struct Assign <: Statement
    var::Var
    expr::Expression
end

struct Block <: Expression
    side_effects::Vector{<:Program}
    result::Expression
end

This makes sure that Expressions (things that evaluate to numbers) and Statements (things that are only side-effects) are properly separated -- you never can create invalid programs like 1 + (x = 2). And it could not be written without abstract types (or mutually recursive types, but they currently do not exist).


From the documentation at https://docs.julialang.org/en/v1/manual/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. While this might at first seem unduly restrictive, it has many beneficial consequences with surprisingly few drawbacks. It turns out that being able to inherit behavior is much more important than being able to inherit structure, and inheriting both causes significant difficulties in traditional object-oriented languages...

So, what you were seeing when you saw " <: when creating a struct" was an example of subclassing the struct from an abstract type, usually in order to use other functions (methods) of that type with the new struct. That is, the code was not creating an abstract supertype. The <: meant that they were creating a concrete type but deriving it (with the <:) from a previously stated abstract type (that would have been to the right of the <: in that case).