I am looking at someone code and it uses <:
when creating a struct. I looked in the docs and found out that this means they are creating an abstract supertype. Can anyone explain what this is and why one would want to use it?
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!"
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 Expression
s (things that evaluate to numbers) and Statement
s (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).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With