Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why create an abstract super type in Julia?

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?

like image 297
logankilpatrick Avatar asked Feb 13 '20 23:02

logankilpatrick


2 Answers

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.

like image 60
David Varela Avatar answered Oct 27 '22 10:10

David Varela


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

like image 35
phipsgabler Avatar answered Oct 27 '22 11:10

phipsgabler