Multiple Dispatch
Prerequisites: The type system and type hierarchy
In Julia, a function is a name, and a method is one implementation of it for particular argument types. A function can have many methods, and when you call it, Julia picks the most specific method based on the types of all the arguments. This is multiple dispatch.
describe(x) = "something"
describe(x::Number) = "a number"
describe(x::Integer) = "an integer"
describe(x::AbstractString) = "a string of length $(length(x))"
println(describe(3))
println(describe(2.5))
println(describe("hey"))
println(describe([1, 2]))
println(length(methods(describe)))
an integer
a number
a string of length 3
something
4
3 matches Any, Number, and Integer; Julia chooses Integer
because it's the most specific.
Dispatch on all arguments
Object-oriented languages pick a method based on one object
(a.collide(b) looks at a only). Julia looks at every argument:
abstract type Thing end
struct Asteroid <: Thing end
struct Ship <: Thing end
collide(a::Thing, b::Thing) = "they bounce"
collide(a::Asteroid, b::Ship) = "the ship is destroyed"
collide(a::Ship, b::Asteroid) = collide(b, a)
collide(a::Asteroid, b::Asteroid) = "the asteroids merge"
println(collide(Ship(), Ship()))
println(collide(Asteroid(), Ship()))
println(collide(Ship(), Asteroid()))
println(collide(Asteroid(), Asteroid()))
they bounce
the ship is destroyed
the ship is destroyed
the asteroids merge
Handling combinations of types like this is awkward with single
dispatch (it needs the "visitor pattern"); here it's four one-line
definitions. (struct defines a new type — the next lesson covers it.)
Extending other people's functions
You can add methods to functions from Julia itself or from packages — including operators, which are ordinary functions:
struct Money
cents::Int
end
Base.:+(a::Money, b::Money) = Money(a.cents + b.cents)
Base.:*(k::Real, m::Money) = Money(round(Int, k * m.cents))
Base.show(io::IO, m::Money) = print(io, "\$", m.cents ÷ 100, ".", lpad(m.cents % 100, 2, "0"))
price = Money(1999)
println(price + Money(1))
println(3 * price)
println(sum([Money(250), Money(125), Money(5)]))
$20.00
$59.97
$3.80
Because sum is written in terms of +, it now works on Money too —
without anyone changing sum. This is why Julia packages compose so
well.
Ambiguity
If two methods are equally specific, Julia refuses to guess:
combine(a::Int, b) = "int first"
combine(a, b::Int) = "int second"
println(combine(1, "x"))
try
combine(1, 2)
catch err
println(typeof(err))
end
combine(a::Int, b::Int) = "both ints"
println(combine(1, 2))
int first
MethodError
both ints
Fix an ambiguity by adding a method for the overlapping case.
Inspecting dispatch
methods(f)lists all methods off.@which f(args...)shows which method a call would use.hasmethod(f, (Int, String))checks whether one exists.
Common mistakes
- Annotating every argument with a concrete type (
x::Float64). Your function then rejectsInts,Float32s, and dual numbers used for automatic differentiation. Annotate with abstract types (Real), or not at all, unless you need to choose between methods. - Adding methods for types you don't own to functions you don't own (type piracy) — it can change behavior for everyone who loads your code.