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.