Static Dispatch

Dispatch is where the same call expression can result in a different function being run, depending on the types of its arguments and/or return value. It's a form of ad hoc polymorphism.

Static dispatch is where only types known at compile time affect which function gets run. This is in contrast to dynamic dispatch, which uses runtime information to decide which function gets run.

Roc's only ad hoc polymorphism system is static dispatch, and dynamic dispatch is unsupported by design. A major reason for this is that Roc's static dispatch has no runtime overhead; after compilation, it's exactly as if you had called the function directly. (In contrast, it's impossible to avoid runtime overhead in dynamic dispatch, because it has to process information at runtime to do the dispatch.)

Methods

A method is a function associated with a type. On a nominal type, methods are defined in the .{ } block after the type declaration:

Counter := { value: I64 }.{
    new : () -> Counter
    new = || { value: 0 }

    increment : Counter -> Counter
    increment = |{ value }| { value: value + 1 }
}
counter : Counter
counter = Counter.new().increment()

Most method names are only used when they are called explicitly. Some names are also recognized by language syntax or builtin APIs. Defining one of those well-known methods opts the type into that syntax or API while still using ordinary static dispatch.

Well-Known Methods

These methods are not dynamic interfaces. The checker resolves each use to a specific method implementation, and later compilation emits a direct call or a derived structural operation. This table is not a limit on method names; packages can define and require their own methods with where clauses.

MethodUsed byImplement when
to_inspect : T -> StrStr.inspect(value)The type needs a custom debug representation.
is_eq : T, T -> Bool==, !=Equality should be available or customized.
to_hash : T, Hasher -> HasherDict, Set, and hash-based APIsValues of the type should participate in hashing.
plus, minus, times, div_by, div_trunc_by, rem_by+, -, *, /, //, %The type has arithmetic-like operations.
is_lt, is_lte, is_gt, is_gte<, <=, >, >=The type has an ordering.
negate, notUnary -, unary !The type has a unary negation or complement operation.
from_numeral : Num.Numeral -> Try(T, [InvalidNumeral(Str)])Number literals with target type TPlain numeric literal syntax should construct the type.
from_quote : Str -> Try(T, [BadQuotedBytes(Str)])Quoted string literals with target type TPlain quoted literal syntax should construct the type.
from_interpolation : Str, Iter((item, Str)) -> TInterpolated string literals with target type TInterpolation should construct the type.
iter : T -> Iter(item)for item in valueThe type should be iterable in for loops.
nextfor loop iteration stepsUsually provided by Iter; collection authors usually implement iter.
parser_for : encoding -> (state -> Try({ value : T, rest : state }, err))Generic parser APIs such as JSON parsingA format should be able to parse the type.
encode_to : T, encoding -> (state -> Try(state, err))Generic encoder APIs such as JSON encodingA format should be able to encode the type.

to_inspect

The to_inspect method customizes how a value is rendered by Str.inspect. Use it for debug, logging, and test-failure output.

Color := [Red, Green, Blue].{
    to_inspect : Color -> Str
    to_inspect = |color| match color {
        Red => "Color.Red"
        Green => "Color.Green"
        Blue => "Color.Blue"
    }
}

When Str.inspect is called on a Color value, it uses Color.to_inspect:

red : Color
red = Red

Str.inspect(red)  # "Color.Red"

Without to_inspect, Str.inspect uses Roc's built-in structural representation for the value.

Equality and Hashing

The is_eq method customizes how equality is checked using the == and != operators.

Point := { x: I64, y: I64 }.{
    is_eq : Point, Point -> Bool
    is_eq = |a, b| a.x == b.x and a.y == b.y
}

When == is used on Point values, it calls the is_eq method:

p1 : Point
p1 = { x: 1, y: 2 }

p2 : Point
p2 = { x: 1, y: 2 }

expect p1 == p2  # calls Point.is_eq(p1, p2)
expect (p1 != p2) == False

For !=, Roc calls is_eq and negates the Bool result.

The to_hash method feeds a value into a Hasher:

to_hash : T, Hasher -> Hasher

Hash-based APIs use to_hash together with is_eq. For example, dictionary keys must be hashable and comparable. If you define custom equality, make sure the hash is consistent with it: equal values must feed the same hash data.

Roc can derive structural equality and hashing for supported structural shapes. Define explicit methods when the derived behavior is not the behavior you want, or when a nominal type should expose a stable custom definition.

Operators

Binary arithmetic operators dispatch to methods on the left operand. The return type is the left operand's type, but the right operand can have a different type if the method signature allows it.

Vec := { x: I64, y: I64 }.{
    plus : Vec, Vec -> Vec
    plus = |a, b| { x: a.x + b.x, y: a.y + b.y }
}

When + is used on Vec values, it calls the plus method:

v1 : Vec
v1 = { x: 1, y: 2 }

v2 : Vec
v2 = { x: 3, y: 4 }

# v1 + v2 calls Vec.plus(v1, v2)

The arithmetic operator mapping is:

OperatorMethod
+plus
-minus
*times
/div_by
//div_trunc_by
%rem_by

Comparison operators dispatch to methods whose result is Bool. Both operands must have the same type.

OperatorMethod
<is_lt
<=is_lte
>is_gt
>=is_gte

Unary operators dispatch to methods whose argument and return type are the same:

OperatorMethod
-xnegate
!xnot
Duration := { millis : I64 }.{
    times : Duration, I64 -> Duration
    times = |duration, scale| { millis: duration.millis * scale }
}

longer : Duration
longer = Duration.{ millis: 10 } * 3

Literal Conversion

Number literals dispatch the from_numeral method when the target type is a nominal type that defines it:

Celsius := { degrees: I64 }.{
    from_numeral : Num.Numeral -> Try(Celsius, [InvalidNumeral(Str)])
    from_numeral = |n| match I64.from_numeral(n) {
        Ok(degrees) => Ok({ degrees })
        Err(err) => Err(err)
    }
}

temp : Celsius
temp = 21  # calls Celsius.from_numeral

Num.Numeral carries the literal's exact digits, so a custom type can accept the literal range its representation supports and reject the rest with InvalidNumeral.

Quoted string literals dispatch from_quote when the target type defines it:

HttpMethod := [Get, Post, Put, Delete].{
    from_quote : Str -> Try(HttpMethod, [BadQuotedBytes(Str)])
    from_quote = |raw| match raw {
        "GET" => Ok(Get)
        "POST" => Ok(Post)
        "PUT" => Ok(Put)
        "DELETE" => Ok(Delete)
        _ => Err(BadQuotedBytes("expected GET, POST, PUT, or DELETE"))
    }
}

method : HttpMethod
method = "POST"  # calls HttpMethod.from_quote

If the method returns Err(BadQuotedBytes(message)), the compiler reports the literal conversion error before the program runs.

Interpolated string literals dispatch from_interpolation based on the result type. The first argument is the literal segment before the first interpolation. The iterator yields each interpolated value paired with the literal segment that follows it.

# For a target type named Html:
from_interpolation : Str, Iter((Html, Str)) -> Html

Plain quoted string segments inside an interpolation are always Str values; the interpolated values are the item type in Iter((item, Str)).

Iteration

A for loop calls iter on the value after in. The iter method must return an Iter(item) whose item type matches the loop pattern.

Rows := { items : List(Row) }.{
    iter : Rows -> Iter(Row)
    iter = |rows| rows.items.iter()
}

for row in rows {
    process(row)
}

The loop then repeatedly calls next on the Iter(item) value:

next : Iter(item) -> [One({ item : item, rest : Iter(item) }), Skip({ rest : Iter(item) }), Done]

Package authors usually implement iter for their collection type and build the returned iterator with the Iter APIs. The next method is the hook on the iterator value itself.

Parsing and Encoding

Generic parser and encoder APIs use parser_for and encode_to to ask a type how it should be read or written for a particular format.

Token := { raw : Str }.{
    parser_for : encoding -> (state -> Try({ value : Token, rest : state }, err))
        where [
            encoding.parse_str : encoding, state -> Try({ value : Str, rest : state }, err),
        ]
    parser_for = |encoding| {
        Encoding : encoding

        |state| {
            parsed = Encoding.parse_str(encoding, state)?
            Ok({ value: Token.{ raw: parsed.value }, rest: parsed.rest })
        }
    }

    encode_to : Token, encoding -> (state -> Try(state, err))
        where [
            encoding.encode_str : encoding, Str, state -> Try(state, err),
        ]
    encode_to = |token, encoding| {
        Encoding : encoding

        |state| Encoding.encode_str(encoding, token.raw, state)
    }
}

Structural records, tag unions, lists, sets, dictionaries, and supported builtins can use derived parser and encoder implementations when the selected format supports their shape. A nominal type can provide explicit parser_for or encode_to methods when it wants a custom representation or when its backing should remain hidden.

Number Literal Defaulting

When nothing in the program pins a literal's type, the compiler commits the first type in Dec, I64, U64, I128, U128, I32, U32, I16, U16, I8, U8, F64, F32 that satisfies all of the literal's constraints. A plain 5 defaults to Dec; a 5 whose surrounding code demands an integer gets the first integer type that fits.

If committing a default narrows a function's inferred type, the compiler emits a LITERAL DEFAULTED warning. To pick a different type, add a type annotation or a suffix (5.U64).