Skip to content
AgileLogics
7 min read

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 of f.
  • @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 rejects Ints, 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.