# Multiple Dispatch

If you remember one chapter from this book, make it this one.

Multiple dispatch is Julia's answer to the question every language has to answer: *when I write `f(x, y)`, which code actually runs?* Most languages answer with "look at the function name" (procedural) or "look at the object on the left of the dot" (object-oriented). Julia answers: **look at the types of all the arguments, and pick the most specific match.**

That sounds like a modest technical variation. It isn't. It changes how you design programs, and it's the reason Julia's ecosystem composes in ways that genuinely surprise people coming from other languages.

## The mechanics

A **function** in Julia is a name. A **method** is one specific implementation of it for a particular combination of argument types. One function, many methods.

```{code-block} julia
struct Bond
    face_value::Float64
    coupon::Float64
end

struct Equity
    shares::Int
    price::Float64
end

value(b::Bond) = b.face_value
value(e::Equity) = e.shares * e.price
```

We just defined one function, `value`, with two methods. Julia picks between them by looking at the argument type:

```{code-block} julia
julia> value(Bond(1000.0, 0.05))
1000.0

julia> value(Equity(100, 45.50))
4550.0

julia> methods(value)
# 2 methods for generic function "value":
[1] value(b::Bond)
[2] value(e::Equity)
```

So far this is just single dispatch — the same thing you'd get from `bond.value()` in Python. Now the interesting part.

## Dispatching on *all* the arguments

```{code-block} julia
abstract type Currency end
struct USD <: Currency; amount::Float64; end
struct EUR <: Currency; amount::Float64; end

convert_amount(from::USD, to::Type{EUR}) = EUR(from.amount * 0.92)
convert_amount(from::EUR, to::Type{USD}) = USD(from.amount * 1.09)
```

Which method runs depends on *both* arguments together. There's no privileged first argument, no "this" object that owns the method.

Consider what that means for a genuinely two-sided operation like multiplication. `A * B` where `A` is a sparse matrix and `B` is a dense vector should run a completely different algorithm from `A * B` where both are dense. In an object-oriented language, where does that code live? On the sparse matrix class? On the vector class? Whoever owns it has to know about the other, and if a third party defines a new matrix type, they can't add a method to *your* class.

In Julia, the method just says what it's for:

```{code-block} julia
*(A::SparseMatrix, b::DenseVector) = ...
```

and it lives wherever it makes sense. The dispatch system handles the rest.

## Specificity: how Julia chooses

When several methods could apply, Julia picks the **most specific** one:

```{code-block} julia
describe(x) = "something"
describe(x::Number) = "a number"
describe(x::Integer) = "an integer"
describe(x::Int64) = "specifically an Int64"
```

```{code-block} julia
julia> describe("hello")
"something"

julia> describe(3.14)
"a number"

julia> describe(big(3))     # BigInt
"an integer"

julia> describe(3)
"specifically an Int64"
```

The rule is intuitive once stated: the method whose signature is a subtype of all the others wins. This lets you write one general fallback plus specialized fast paths for the cases you care about — and crucially, adding a specialization later doesn't require touching the general version.

If two methods are equally specific and neither dominates, Julia raises an ambiguity error rather than guessing. That's occasionally annoying and always correct.

## Extending functions you didn't write

This is where the practical magic is. You can add methods to functions defined in Base or in someone else's package.

```{code-block} julia
struct Money
    cents::Int
end

# Teach Julia how to add two Money values
Base.:+(a::Money, b::Money) = Money(a.cents + b.cents)

# And how to print one
Base.show(io::IO, m::Money) = print(io, "\$", m.cents ÷ 100, ".", lpad(m.cents % 100, 2, '0'))
```

```{code-block} julia
julia> Money(1250) + Money(399)
$16.49
```

We didn't subclass anything. We didn't wrap anything. We added two methods to two existing functions, and now our type participates in the language's normal syntax.

Keep going, and it starts to compound:

```{code-block} julia
Base.zero(::Type{Money}) = Money(0)
Base.:*(n::Integer, m::Money) = Money(n * m.cents)
Base.isless(a::Money, b::Money) = a.cents < b.cents
```

```{code-block} julia
julia> wallet = [Money(1250), Money(399), Money(2000)]

julia> sum(wallet)
$36.49

julia> sort(wallet)
3-element Vector{Money}:
 $3.99
 $12.50
 $20.00

julia> maximum(wallet)
$20.00
```

Look carefully at what just happened. I never wrote a `sum` for `Money`. I never wrote a `sort`. Those functions in Base were written generically — `sum` needs `+` and `zero`, `sort` needs `isless` — and by supplying those three small methods, my type inherited every algorithm in the standard library that's built on them.

:::{note} This is the composability everyone talks about
Here's the story Julia programmers tell, and it's true. Someone writes a differential equation solver. Someone else, who has never met them, writes a package for numbers with physical units. A third person writes a package for numbers that carry uncertainty. Nobody coordinates.

Then you solve a differential equation with uncertain, unit-carrying numbers and it just works — because the solver was written against `+`, `*`, and `<`, and the number packages supplied methods for those. In a language built on classes and inheritance, this requires each library to explicitly anticipate the others. In Julia it falls out of the dispatch model.

It doesn't work 100% of the time. But it works often enough to be a real, load-bearing advantage of the language.
:::

## Dispatch and speed

There's a natural worry here: if Julia has to decide which method to run, isn't that a runtime cost on every single call?

Almost never. When the compiler specializes a function for a set of concrete argument types, it knows exactly which method will be selected, and it wires in a direct call — often inlining it entirely. The dispatch happened at compile time.

The exception is when the compiler *can't* know the types — because a value came from an untyped global, or a `Vector{Any}`, or a function whose return type it couldn't infer. Then Julia falls back to **dynamic dispatch**, looking up the method at runtime. That's the thing that makes Julia code slow, and it's what we'll learn to detect in the *Performance* chapter.

So: dispatch is free when types are inferable, expensive when they aren't. This is the same lesson as everywhere else in this language, arriving from a new direction.

## Designing with dispatch

Coming from OOP, the instinct is to reach for a class hierarchy with inherited behaviour. Julia doesn't offer that, and the replacement is a different way of thinking:

**Define the interface as a set of functions.** Rather than a base class with methods, decide which *functions* a type must support. Base does this all the time — to make your type iterable, define `iterate`. To make it indexable, define `getindex`, `setindex!`, and `size`. There's no formal `interface` keyword; the contract is documented and enforced by whether things work.

**Use abstract types for grouping, generic functions for behaviour.**

```{code-block} julia
abstract type Instrument end

# The general contract: every Instrument must implement value()
value(x::Instrument) = error("value not implemented for $(typeof(x))")

# Generic code written once, against the abstraction
portfolio_value(xs) = sum(value, xs)
riskiest(xs) = argmax(value, xs)
```

New instrument types slot in by defining `value`, and every generic function above works on them immediately.

**Prefer small, focused functions.** Dispatch works on functions, so the more your logic is expressed as functions rather than branching inside one big function, the more extensible it is. An `if typeof(x) == ...` chain is dispatch you're doing by hand — and worse, it's a closed set that nobody else can extend.

## A worked example

Let's build a tiny expense-classification system and watch dispatch do the work of a class hierarchy.

```{code-block} julia
abstract type Expense end

struct Travel <: Expense
    amount::Float64
    miles::Float64
end

struct Meal <: Expense
    amount::Float64
    attendees::Int
end

struct Software <: Expense
    amount::Float64
    annual::Bool
end

# Each type answers the same questions differently
deductible(e::Travel)   = e.amount
deductible(e::Meal)     = e.amount * 0.5           # 50% rule
deductible(e::Software) = e.annual ? e.amount / 12 : e.amount

# Generic reporting, written once
total_deductible(xs) = sum(deductible, xs)

# A fallback for anything we haven't specialized
deductible(e::Expense) = e.amount
```

```{code-block} julia
julia> expenses = Expense[Travel(450.0, 300.0), Meal(120.0, 3), Software(1200.0, true)]

julia> total_deductible(expenses)
610.0
```

Adding a new expense category tomorrow means writing one struct and one `deductible` method. Nothing else changes. No base class to modify, no `if` chain to find and update, no risk of forgetting a branch.

## Try it yourself

1. Define a `Temperature` struct holding a `Float64` in Celsius. Add methods so `+` works between two `Temperature`s and `show` prints something like `21.5°C`.
2. Define `describe(x)` with methods for `String`, `Number`, and a fallback for anything else. Test all three.
3. Add `Base.isless` for `Temperature` and confirm that `sort` and `maximum` now work on a vector of them without you writing either.
4. Look at `methods(+)` and count how many methods Julia ships for addition. Then pick one and read it with `@which 1 + 2`.

### Solutions

```{code-block} julia
# 1.
struct Temperature
    celsius::Float64
end
Base.:+(a::Temperature, b::Temperature) = Temperature(a.celsius + b.celsius)
Base.show(io::IO, t::Temperature) = print(io, t.celsius, "°C")

# 2.
describe(x) = "some other thing: $(typeof(x))"
describe(x::String) = "a string of length $(length(x))"
describe(x::Number) = "a number, specifically $(typeof(x))"

# 3.
Base.isless(a::Temperature, b::Temperature) = a.celsius < b.celsius
sort([Temperature(21.5), Temperature(18.0), Temperature(30.2)])
maximum([Temperature(21.5), Temperature(18.0)])

# 4.
length(methods(+))     # a few hundred, and every one is a specialization
@which 1 + 2           # +(x::T, y::T) where T<:BitInteger  — in Base
```

That last exercise is worth actually running. Seeing that `+` has hundreds of methods, and that `@which` will tell you exactly which one your call landed on, makes the whole model concrete in a way that no amount of explanation does.

Next we'll look at how to organize your code into modules and packages — and how Julia's environment system keeps your projects from stepping on each other.
