Skip to content

Functions

Bodies, parameters, defaults, mutable parameters, return types, fallible signatures, effect rows, generics, methods, and function values.

Updated View as Markdown

fn declares a callable unit. The same keyword introduces free functions, methods, static functions, trait requirements, and prelude signatures. Function and parameter names use snake_case.

fn add(a: int, b: int): int {
    return a + b
}

fn main(): void {
    println("${add(20, 22)}")
}

Bodies

A block body holds statements, early returns, and an optional final tail expression. The tail expression is the result — no return keyword needed.

fn absolute(value: int): int {
    if value < 0 {
        return -value
    }
    value
}

fn category(value: int): string {
    if value < 0 {
        return "negative"
    }
    if value == 0 {
        return "zero"
    }
    "positive"
}

fn main(): void {
    println("${absolute(-7)} ${category(0)} ${category(3)}")
}

When the whole body is one expression, use => instead of braces.

fn double(value: int): int => value * 2

fn is_even(value: int): bool => value % 2 == 0

fn describe(value: int): string => "${value} is even: ${is_even(value)}"

fn main(): void {
    println(describe(double(21)))
}

An => body is checked against the declared return type exactly like a block’s tail expression. It may be any expression, including an if or a match:

enum Level { Low, High }

fn threshold(l: Level): int => match l {
    Low => 10
    High => 90
}

fn clamp_high(value: int): int => if value > 90 { 90 } else { value }

fn main(): void {
    println("${threshold(High)} ${clamp_high(120)}")
}

Give every normally completing path a value. A path that returns, propagates an error, or otherwise diverges does not need to produce the block’s tail value, so an early-return branch never forces the remaining branch to manufacture a placeholder.

Parameters

Parameters are name-and-type pairs, and calls supply them positionally.

fn clamp(value: int, minimum: int, maximum: int): int {
    if value < minimum { return minimum }
    if value > maximum { return maximum }
    return value
}

fn main(): void {
    println("${clamp(150, 0, 100)} ${clamp(-5, 0, 100)} ${clamp(42, 0, 100)}")
}

Argument count is checked exactly:

fn add(a: int, b: int): int => a + b

fn main(): void {
    println("${add(1)}")
}

Named call arguments and variadic parameters are not part of the language. When an argument list grows past what a reader can keep straight, take a struct instead of adding more positional parameters.

struct ConnectOptions {
    host: string
    port: int
    secure: bool
    timeout_ms: int
}

fn connect(options: ConnectOptions): string {
    scheme := if options.secure { "https" } else { "http" }
    return "${scheme}://${options.host}:${options.port} (${options.timeout_ms}ms)"
}

fn main(): void {
    println(connect(ConnectOptions {
        host: "db.internal",
        port: 6432,
        secure: false,
        timeout_ms: 250,
    }))
}

Defaults

A parameter may carry a default value. Defaults fill an omitted trailing suffix, left to right.

const DEFAULT_PORT: int = 5432

fn connect(host: string, port: int = DEFAULT_PORT, secure: bool = true): string {
    scheme := if secure { "https" } else { "http" }
    return "${scheme}://${host}:${port}"
}

fn main(): void {
    println(connect("db.internal"))
    println(connect("db.internal", 6432))
    println(connect("db.internal", 6432, false))
}

Because calls are positional, a caller cannot skip port and supply only secure. Once a parameter has a default, every parameter after it must have one too:

fn f(a: int = 1, b: int): int => a + b

fn main(): void {
    println("${f(1, 2)}")
}

Default expressions fold at compile time. They may use literals and constants, but they cannot read the caller’s runtime state.

Mutable parameters

Prefix a parameter with mut when the body assigns to it or mutates through it. The final value is written back to the caller’s place.

fn bump(mut value: int): int {
    value += 1
    return value
}

fn append_marker(mut values: []string): void {
    values.add("done")
}

fn main(): void {
    mut names := ["start"]
    append_marker(names)
    println("${bump(41)} ${names.len()} ${names[1] ?? "?"}")
}

The argument has to be a mutable place — a mut binding, or a field or index of one. An immutable binding or a literal cannot satisfy the contract:

fn append_marker(mut values: []string): void {
    values.add("done")
}

fn main(): void {
    names := ["start"]
    append_marker(names)
    println("${names.len()}")
}

Return types

Write : T after the parameter list. void is the spelling for “no useful result”; Unit is accepted as a synonym, and the annotation may be omitted entirely.

fn answer(): int {
    return 42
}

fn log_ready(): void {
    println("ready")
}

fn log_done(): Unit {
    println("done")
}

fn total(xs: []int) {
    mut sum := 0
    for x in xs {
        sum += x
    }
    return sum
}

fn main(): void {
    log_ready()
    log_done()
    println("${answer()} ${total([1, 2, 3])}")
}

total has no annotation, so the checker infers int from its return. That is convenient inside a file and risky across one: an edit to the body silently changes the API. State the type on anything other code depends on.

Record results

A function can return an anonymous record when the caller only needs a few fields together and a named struct would be ceremony. The type is structural: any record with the same field names and types is the same type, so a second declaration can accept the result without either one naming a struct.

fn metrics(xs: []int): { count: int, total: int, empty: bool } {
    mut total := 0
    for x in xs {
        total += x
    }
    return { count: xs.len(), total: total, empty: xs.is_empty() }
}

fn describe(m: { count: int, total: int, empty: bool }): string {
    if m.empty {
        return "no samples"
    }
    return "${m.count} samples, total ${m.total}"
}

fn main(): void {
    println(describe(metrics([3, 4, 5])))
    println(describe(metrics([])))

    m := metrics([1, 2])
    println("${m.count} ${m.total} ${m.empty}")
}

Repeat the field list once too often and a named struct has become the better trade — the record form is for a result that only one or two call sites ever destructure.

The ::result projection

fnname::result names a declaration’s success type, so a helper can follow a signature without restating it.

fn version_code(): int => 140

fn is_supported(v: version_code::result): bool => v >= 100

fn main(): void {
    println("${is_supported(version_code())}")
}

Today the projection only survives lowering when the projected type is a scalar. Naming an aggregate result — a struct, a list, or an anonymous record — through ::result passes the type checker and then fails to build with ATOLL5005, so write those types out until the backend catches up. Its sibling fnname::err, covered under fallible functions, has no such restriction.

Fallible functions

A function that can fail declares T ! E: T on success, E in the error channel.

error ParseError { Empty, NotANumber }

fn parse_port(text: string): int ! ParseError {
    if text.len() == 0 {
        error Empty
    }
    match text.to_int() {
        Some(v) => return v
        None => error NotANumber
    }
}

fn open(text: string): string ! ParseError {
    port := parse_port(text)?
    return "listening on ${port}"
}

fn main(): void {
    println(open("8080").unwrap_or("bad port"))
    println(open("nope").unwrap_or("bad port"))
}

Inside the body, return and the tail expression produce the success type, and error Variant exits through the error channel. At a call site the value is a Result[T, E]: ? propagates it, catch converts it, and unwrap_or supplies a fallback.

The error type of a declaration can be named without repeating it, using the ::err projection — ::result does the same for the success type:

error LoadError { Missing, Corrupt }

fn load(id: int): string ! LoadError {
    if id < 0 {
        error Missing
    }
    return "record ${id}"
}

fn explain(e: load::err): string {
    match e {
        Missing => return "not found"
        Corrupt => return "damaged"
        _ => return "unknown"
    }
}

fn main(): void {
    text := load(-1) catch {
        err => {
            println(explain(err))
            return
        }
    }
    println(text)
}

The catch arm above returns, so text is a plain string. An arm that produces a value instead rewrites the error channel and leaves another Result — see Catch.

Note the binding name: error is a keyword, so an arm cannot bind error — use err or e. Full detail lives in Errors.

Effect rows

A signature may declare the effects its body performs, in square brackets after using. The five effects are Suspend, Error, Alloc, Spawn, and Cancel.

error TaskError { Cancelled }

fn allocating(): []int {
    return [1, 2, 3]
}

fn failing(v: int): int ! TaskError {
    if v < 0 {
        error Cancelled
    }
    return v
}

fn concurrent(): int {
    left := spawn { 20 }
    right := spawn { 22 }
    return left.await() + right.await()
}

fn main(): void {
    println("${allocating().len()} ${concurrent()} ${failing(1).unwrap_or(0)}")
}

A row is a contract for readers and for API review. When it is omitted the compiler infers the union transitively through the call graph, and a row that under-states what the body reaches produces a warning rather than blocking the build — so write the row on exported functions, where the diagnostic is the point.

Generics

Type parameters go in square brackets after the name. Bounds are written inline or in a where clause; the two forms mean the same thing.

fn identity[T](value: T): T => value

fn larger[T: Comparable[T]](a: T, b: T): T {
    if a > b { return a }
    return b
}

fn best[T](xs: []T): T? where T: Comparable[T] {
    return xs.max()
}

fn pair_up[T, U](a: T, b: U): (T, U) => (a, b)

fn main(): void {
    println("${identity(7)} ${larger(2, 9)} ${best([3, 9, 4]) ?? 0}")
    println("${identity("text")} ${larger("alpha", "beta")}")

    p := pair_up(1, "one")
    println("${p.0}=${p.1}")
}

Every call selects its own substitution: a call with int does not specialize the declaration for a later call with string.

A bound is a promise the argument type has to keep, so a generic function is only as usable as the implementations that exist. Display is instructive: the prelude implements it for string and bool, and a type gains it by writing the impl. String interpolation of an int goes through a builtin path rather than that trait, so a T: Display bound will not accept one.

struct Tag { name: string }

impl Display for Tag {
    fn to_string(self): string => "#${self.name}"
}

fn labelled[T](value: T): string where T: Display => "value=${value.to_string()}"

fn main(): void {
    println(labelled(Tag { name: "release" }))
    println(labelled("plain"))
    println(labelled(true))
}

See Generics.

Methods

A function whose first parameter is self or mut self is a method. It can be declared directly in the type body.

struct Counter {
    value: int
    step: int

    fn current(self): int => self.value

    fn increment(mut self): void {
        self.value += self.step
    }

    fn advanced(self, times: int): Counter =>
        Counter { value: self.value + self.step * times, step: self.step }
}

fn main(): void {
    mut c := Counter { value: 0, step: 5 }
    c.increment()
    c.increment()
    println("${c.current()} ${c.advanced(3).value}")
}
Receiver Capability
no self static function, called through the type
self read fields and call non-mutating behaviour
mut self assign through the receiver

A function inside a type body without self is static:

struct Point {
    x: float
    y: float

    fn zero(): Point => Point { x: 0.0, y: 0.0 }
    fn unit_x(): Point => Point { x: 1.0, y: 0.0 }
}

fn main(): void {
    o := Point.zero()
    println("${o.x} ${Point.unit_x().x}")
}

The same methods can come from an impl block:

struct Vec2 { x: float, y: float }

impl Vec2 {
    fn zero(): Vec2 => Vec2 { x: 0.0, y: 0.0 }
    fn length(self): float => (self.x * self.x + self.y * self.y).sqrt()
    fn scaled(self, k: float): Vec2 => Vec2 { x: self.x * k, y: self.y * k }
}

fn main(): void {
    v := Vec2 { x: 3.0, y: 4.0 }
    println("${v.length()} ${v.scaled(2.0).y} ${Vec2.zero().x}")
}

Write the concrete type name as the result, not Self. Self does not unify with the implementing type in an inherent impl:

struct Vec2 { x: float, y: float }

impl Vec2 {
    fn scaled(self, k: float): Self => Vec2 { x: self.x * k, y: self.y * k }
}

fn main(): void {
    println("${Vec2 { x: 1.0, y: 1.0 }.scaled(2.0).x}")
}

Receiver-prefix functions

A method can also be declared at the top level by prefixing the receiver type to the name. This attaches behaviour from the module that needs it, without reopening the type declaration.

struct Reading { celsius: float }

fn Reading.fahrenheit(self): float => self.celsius * 1.8 + 32.0

fn Reading.warmer(self, by: float): Reading => Reading { celsius: self.celsius + by }

fn Reading.reset(mut self): void {
    self.celsius = 0.0
}

fn main(): void {
    mut r := Reading { celsius: 20.0 }
    println("${r.fahrenheit()} ${r.warmer(5.0).celsius}")
    r.reset()
    println("${r.celsius}")
}

The receiver-prefix form needs a self parameter. A fn Type.name() with no receiver parses and type-checks as a static function, but the backend cannot lower a call to it:

struct Reading { celsius: float }

fn Reading.freezing(): Reading => Reading { celsius: 0.0 }

fn main(): void {
    println("${Reading.freezing().celsius}")
}

Declare statics in the type body or in an impl block instead — both lower fine, and the call site is identical:

struct Reading { celsius: float }

impl Reading {
    fn freezing(): Reading => Reading { celsius: 0.0 }
    fn boiling(): Reading => Reading { celsius: 100.0 }
}

fn Reading.fahrenheit(self): float => self.celsius * 1.8 + 32.0

fn main(): void {
    println("${Reading.freezing().fahrenheit()} ${Reading.boiling().fahrenheit()}")
}

Call syntax is value.method() or Type.function() regardless of which of the three declaration sites was used. Trait implementations are covered in Implementations.

Function values

fn(A, B) -> R is the type of a function value. Named functions and closures both inhabit it.

fn double(value: int): int => value * 2
fn triple(value: int): int => value * 3

fn apply(value: int, operation: fn(int) -> int): int {
    return operation(value)
}

fn apply_all(value: int, operations: []fn(int) -> int): []int {
    mut out: []int = []
    for op in operations {
        out.add(op(value))
    }
    return out
}

fn main(): void {
    println("${apply(21, double)} ${apply(21, v => v + 1)}")
    results := apply_all(2, [double, triple])
    println("${results[0] ?? 0} ${results[1] ?? 0}")
}

Function values are ordinary values: they go in struct fields, get returned, and get selected at run time.

struct Handler {
    name: string
    run: fn(int) -> int
}

fn pick(mode: string): fn(int) -> int {
    if mode == "double" {
        return v => v * 2
    }
    return v => v + 1
}

fn main(): void {
    h := Handler { name: "double", run: pick("double") }
    println("${h.name} ${h.run(21)} ${pick("inc")(41)}")
}

A function type may also be fallible. Compatibility covers parameter types, the success type, and the error type — a closure is never erased into an untyped callback. A value of fallible function type has the Result spelled out, because that is what a call to it produces:

error MathError { DivideByZero }

fn checked_div(a: int, b: int): int ! MathError {
    if b == 0 {
        error DivideByZero
    }
    return a / b
}

fn checked_mod(a: int, b: int): int ! MathError {
    if b == 0 {
        error DivideByZero
    }
    return a % b
}

fn run(op: fn(int, int) -> Result[int, MathError], a: int, b: int): string {
    value := op(a, b) catch {
        err => return "error"
    }
    return "${value}"
}

fn main(): void {
    println(run(checked_div, 10, 2))
    println(run(checked_mod, 10, 3))
    println(run(checked_div, 10, 0))
}

The T ! E spelling belongs to a declaration’s return type; Result[T, E] is the type of the value that declaration hands back, and so is what a fn(...) -> ... type needs.

Closures are documented in Closures; call mechanics in Calls.

A complete example

Defaults, methods, receiver-prefix functions, a fallible signature, and a function parameter in one file:

const DEFAULT_WIDTH: int = 24

error ReportError { NoSamples, Negative }

struct Series {
    label: string
    points: []float

    fn total(self): float {
        mut sum := 0.0
        for p in self.points {
            sum += p
        }
        return sum
    }

    fn mean(self): float ! ReportError {
        if self.points.is_empty() {
            error NoSamples
        }
        return self.total() / self.points.len().to_float()
    }
}

fn Series.peak(self): float? => self.points.max()

fn bar(fraction: float, width: int = DEFAULT_WIDTH): string {
    mut filled := (fraction * width.to_float()).to_int()
    if filled < 0 { filled = 0 }
    if filled > width { filled = width }

    mut out := ""
    for i in 0..width {
        if i < filled { out += "#" } else { out += "." }
    }
    return out
}

fn render(s: Series, scale: fn(float) -> float): string ! ReportError {
    avg := s.mean()?
    if avg < 0.0 {
        error Negative
    }
    top := s.peak() ?? 1.0
    return "${s.label} ${bar(scale(avg) / top)} avg=${avg}"
}

fn main(): void {
    series := [
        Series { label: "cpu", points: [0.2, 0.6, 0.4] },
        Series { label: "mem", points: [] },
    ]

    for s in series {
        line := render(s, v => v) catch {
            err => {
                println("${s.label} (no data)")
                continue
            }
        }
        println(line)
    }
}

API boundaries

Bodies can stay expression-oriented and heavily inferred. Exported signatures should do the opposite — the following are all compatibility surface, and a caller can break on any of them:

  • parameter order, types, and mut;
  • the success type and the error type;
  • parameter defaults, because existing calls keep compiling and change behaviour;
  • generic parameters and their bounds;
  • the declared effect row.

When several optional settings start evolving together, replace the trailing default suffix with an options struct: adding a field is then an additive change instead of a new positional parameter.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close