Skip to content
AgileLogics
7 min read

Generic Programming, Traits, and Custom Arrays

Prerequisites: Type stability

Well-written Julia functions are generic: they work for any type that supports the operations they use, and the compiler specializes them for each concrete type.

function mean_generic(xs)
    s = zero(eltype(xs))
    for x in xs
        s += x
    end
    return s / length(xs)
end

println(mean_generic([1, 2, 3, 4]))
println(mean_generic(Float32[1, 2]))
println(mean_generic([1//2, 1//3]))
println(mean_generic([1 + 2im, 3 - 2im]))
println(mean_generic(big.([1, 2])))
2.5
1.5
5//12
2.0 + 0.0im
1.5

One definition, five number types — and each version is as fast as a hand-written specialization.

Holy traits

Sometimes you want to dispatch on a property of a type that isn't captured by the type hierarchy. The trait pattern (named after Tim Holy) uses a function that returns a type describing the property:

abstract type Sound end
struct Barks <: Sound end
struct Meows <: Sound end
struct Silent <: Sound end

struct Dog end
struct Cat end
struct Fish end

Sound(::Type) = Silent()
Sound(::Type{Dog}) = Barks()
Sound(::Type{Cat}) = Meows()

speak(x) = speak(Sound(typeof(x)), x)
speak(::Barks, x) = "woof"
speak(::Meows, x) = "meow"
speak(::Silent, x) = "..."

println(speak(Dog()), " ", speak(Cat()), " ", speak(Fish()))
woof meow ...

Because Sound(typeof(x)) is known at compile time, the extra dispatch costs nothing at run time. Julia's own IndexStyle and Base.IteratorSize are traits; a type can opt in to a trait without changing its supertype.

A custom array type

Subtype AbstractArray and implement size and getindex, and your type gets indexing, iteration, printing, broadcasting, sum, linear algebra, and more for free:

struct OneHot <: AbstractVector{Bool}
    n::Int
    hot::Int
end

Base.size(v::OneHot) = (v.n,)
Base.getindex(v::OneHot, i::Int) = (checkbounds(v, i); i == v.hot)

v = OneHot(5, 2)
display(v)
println(sum(v), " ", findfirst(v), " ", v .* 10)
println([1 2 3 4 5] * v)
5-element OneHot:
 0
 1
 0
 0
 0
1 2 [0, 10, 0, 0, 0]
[2]

The one-hot vector stores two integers instead of five booleans, yet it works everywhere an AbstractVector does. Packages like OffsetArrays, StaticArrays, CUDA.jl (GPU arrays), and SparseArrays are built the same way.

Promotion rules

Julia's numeric promotion is itself extensible. When you define a new number type, Base.promote_rule and Base.convert let it mix with existing numbers in arithmetic — the way Rational and Complex do.