Skip to main content

Multiple Dispatch

Functions can be defined multiple times for different kinds of input:

fn describe(x: Integer) { "a whole number" }
fn describe(x: Float64) { "a decimal number" }

test describe(1) == "a whole number"
test describe(1.5) == "a decimal number"

From the outside, describe looks like one function. When called, it switches to the right implementation based on the arguments – all of them, not just the first, which is why this is called multiple dispatch.

Because signatures are full patterns, you can dispatch on values and structure, not just types:

fn fib(n) { fib(n-1) + fib(n-2) }
fn fib(1) { 1 }
fn fib(0) { 0 }

Which definition wins?

When several definitions match, the most recently defined one wins. The idiom is to write generic methods first and specific ones later – a fallback first, then the fast paths and special cases:

fn foo(x: Number) { "a number" }
fn foo(x: Integer) { "a round number" }

foo(3.14) # "a number"
foo(2) # "a round number"

Why dispatch?

The main goal is polymorphism: getting different kinds of objects to behave alike. When you write for x = xs, the language calls iterate(xs) and next(&itr), and those functions do the right thing whether xs is a list, a string, a range or a JavaScript array. Any type you define can join in by adding methods to the same functions.

Unlike class methods, dispatch doesn't privilege the first argument, and – crucially – anyone can add methods to anyone else's function. Your library can make its types printable by extending show, iterable by extending iterate, or addable by extending +, without touching the code that defined those functions.

@extend

Within a module, defining fn foo(...) creates a fresh function named foo. To add methods to a function that comes from another module – including the standard library – mark the definition with @extend:

bundle Celsius(deg)

@extend
fn show(Celsius(deg)) {
print(deg)
print("°C")
}
caution

If you forget @extend, you silently get a new local function with the same name, and your method will never be called – there's no error. If a method mysteriously isn't kicking in, this is the first thing to check.

Traits

Traits let dispatch work over open-ended families of types. The standard library defines Number, Real and Integer this way: a trait is a tag, and types opt in by extending matchTrait:

@extend
fn matchTrait($Number, x: Complex) { Some(x) }

With that one line, every function defined over x: Number+, abs, sum, and so on – now accepts Complex. See Pattern Matching for defining your own.