Skip to content

Calls

Call forms, evaluation order, defaults, mutable arguments, method and static dispatch, explicit generic arguments, trailing closures, errors, and effects.

Updated View as Markdown

A call supplies one value for each parameter in a callable’s static signature. Free functions, methods, static functions, function values, and closures all use the same rules.

struct User {
    first: string
    last: string

    fn display_name(self): string => "${self.first} ${self.last}"
    fn anonymous(): User => User { first: "anon", last: "" }
}

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

fn main(): void {
    total := add(20, 22)
    name := User { first: "Ada", last: "Lovelace" }.display_name()
    fallback := User.anonymous()
    empty := List.new[string]()
    mapped := [1, 2, 3].map { value => value * 2 }

    println("${total} ${name} ${fallback.first} ${empty.len()} ${mapped.len()}")
}

Forms

Form Selects
function(args) A named function in scope
value.method(args) A method for the receiver’s static type
Type.function(args) A static function on the type
callable(args) A function or closure value
function[Types](args) A call with explicit generic arguments
function(args) { closure } A call whose last argument is a trailing closure

Parentheses are required even when there are no arguments — a bare name is a function value, not a call.

fn now(): int => 7

fn apply(op: fn() -> int): int => op()

fn main(): void {
    called := now()
    as_value := now
    println("${called} ${apply(as_value)} ${apply(now)}")
}

Using the name where a value is not expected is a type error, which is the diagnostic you get if you forget the parentheses:

fn now(): int => 7

fn main(): void {
    v: int = now
    println("${v}")
}

Line breaks

A ( at the start of a fresh line does not continue the previous expression. operation on one line and (value) on the next are two statements, and the first one is just a function value:

fn twice(v: int): int => v * 2

fn main(): void {
    value := 3
    result: int = twice
    (value)
    println("${result}")
}

Keep ( on the callee’s line. Inside an already open argument list, line breaks are ordinary trivia, and a trailing comma is allowed:

fn combine(first: string, second: string, third: string): string =>
    first + "-" + second + "-" + third

fn main(): void {
    joined := combine(
        "alpha",
        "beta",
        "gamma",
    )
    println(joined)
}

A member chain may also break across lines, because the leading . makes the connection explicit:

fn main(): void {
    result := [3, 1, 2]
        .sorted()
        .map { v => v * 10 }
        .len()
    println("${result}")
}

Evaluation

Call evaluation is deterministic:

  1. evaluate the callee or receiver, once;
  2. evaluate the written arguments left to right, once each;
  3. materialize omitted trailing defaults;
  4. enter the callable;
  5. produce its success value, error value, or suspension.
fn step(label: string, value: int): int {
    println("evaluating ${label}")
    return value
}

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

fn main(): void {
    total := combine(step("first", 20), step("second", 22))
    println("total ${total}")
}

That prints evaluating first, then evaluating second, then total 42. The compiler does not reorder calls because one looks pure, and method lookup never executes user code.

The whole argument list is established before the callee body begins, so a later argument cannot observe mutation performed inside the callee, and a call that fails while evaluating an argument never enters the callee at all.

Arity and defaults

Calls are positional: supply every required parameter and no extras.

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

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

Defaults fill only an omitted trailing suffix, so a caller cannot skip port and supply only secure:

fn connect(host: string, port: int = 5432, 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))
}

There are no named arguments and no variadic parameters — name: value inside an argument list is not call syntax, and the diagnostic reflects that the compiler is trying to read it as an ordinary expression:

fn connect(host: string, port: int): string => "${host}:${port}"

fn main(): void {
    println(connect(port: 5432, host: "db.internal"))
}

Default expressions are folded at compile time; they do not execute in the caller’s scope. Changing a public default changes the behaviour of source that still compiles unchanged, so treat it as API surface. When several optional settings evolve independently, take a struct — the field names then do the work named arguments would have:

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

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

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

Arguments

Each argument is checked against its parameter’s expected type, and that expectation flows into the argument. Numeric literals adopt the parameter’s width, and a closure argument’s parameter types come from the function type.

fn write(port: u16, transform: fn(string) -> string): string {
    return transform("  payload  ") + "@${port}"
}

fn main(): void {
    println(write(443, text => text.trim()))
}

443 is created as a u16, and text is known to be a string without an annotation. A mismatched argument is reported in argument position:

fn takes_int(value: int): int => value

fn main(): void {
    println("${takes_int("nope")}")
}

Mutable arguments

A mut parameter is an in-out place contract. The callee receives the current value and its final value is written back.

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

fn bump(mut counter: int, by: int): void {
    counter += by
}

fn main(): void {
    mut names := ["start"]
    mut total := 10

    append_marker(names)
    bump(total, 5)

    println("${names.len()} ${names[1] ?? "?"} ${total}")
}

The argument must be a mutable place — a mut binding, or a field or index of one. A plain binding cannot satisfy it:

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

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

Mutation through mut self uses the same model, and the receiver is still evaluated once:

struct Queue {
    items: []int

    fn push(mut self, value: int): void {
        self.items.add(value)
    }

    fn len(self): int => self.items.len()
}

fn main(): void {
    mut q := Queue { items: [] }
    q.push(1)
    q.push(2)
    println("${q.len()}")
}

Generic arguments

Generic arguments are normally inferred from the argument types and the expected result:

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

fn first_or[T](xs: []T, fallback: T): T => xs.get(0) ?? fallback

fn main(): void {
    println("${identity(42)} ${identity("text")}")
    println("${first_or([3, 4], 0)} ${first_or([], "none")}")
}

Write them explicitly, immediately before (, when nothing constrains the choice — most often when constructing an empty collection:

fn empty_of[T](): []T => []

fn main(): void {
    names := List.new[string]()
    counts := Map.new[string, int]()
    seen := Set.new[int]()
    blank := empty_of[float]()

    println("${names.len()} ${counts.size()} ${seen.size()} ${blank.len()}")
}

Square brackets not followed by ( remain indexing syntax, so the two forms never collide. Each call receives an independent substitution: a call with int does not specialize the declaration for a later call with string.

Method and static dispatch

Method selection uses the receiver’s static type together with inherent methods, receiver-prefix declarations, applicable trait implementations, and bounds in scope. It is resolved at compile time — there is no runtime method table.

struct Article {
    title: string
    body: string
}

impl Article {
    fn summary(self): string => "${self.title} (${self.body.len()} chars)"
}

fn Article.headline(self): string => self.title

trait Sized {
    fn approximate_size(self): int
}

impl Sized for Article {
    fn approximate_size(self): int => self.title.len() + self.body.len()
}

fn report[T](item: T): string where T: Sized => "size ${item.approximate_size()}"

fn main(): void {
    a := Article { title: "Atoll", body: "a systems language" }
    println(a.summary())
    println(a.headline())
    println(report(a))
}

Missing selection is a compile-time error naming the receiver type:

struct Point { x: float, y: float }

fn main(): void {
    p := Point { x: 1.0, y: 2.0 }
    println("${p.magnitude()}")
}

Static functions have no receiver and are selected through the type:

struct Point {
    x: float
    y: float

    fn zero(): Point => Point { x: 0.0, y: 0.0 }
    fn along_x(distance: float): Point => Point { x: distance, y: 0.0 }
}

fn main(): void {
    println("${Point.zero().x} ${Point.along_x(3.5).x}")
}

Calling function values

A local, parameter, or field with function type is called with the same parentheses. The callable expression is evaluated once, so a call that produces a function can be invoked immediately:

struct Route {
    path: string
    handler: fn(int) -> int
}

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

fn main(): void {
    r := Route { path: "/scale", handler: choose_handler("double") }
    println("${r.handler(21)}")
    println("${choose_handler("inc")(41)}")
}

choose_handler("inc") runs first and yields a function value; the second parentheses invoke it.

Trailing closures

When the last argument is a closure, it may be written in braces after the argument list instead of inside it. Both forms mean the same thing.

fn retry(times: int, body: fn(int) -> bool): bool {
    for attempt in 0..times {
        if body(attempt) {
            return true
        }
    }
    return false
}

fn main(): void {
    inline_form := retry(3, attempt => attempt == 2)
    trailing_form := retry(3) { attempt => attempt == 2 }
    println("${inline_form} ${trailing_form}")
}

The braces belong to the closure argument, not to the call’s result. This is what makes multi-line callbacks readable, and it is how the collection higher-order methods are normally written:

fn each_path(paths: []string, body: fn(string) -> void): void {
    for p in paths {
        body(p)
    }
}

fn main(): void {
    values := [1, -2, 3, -4]

    positive := values.filter { v => v > 0 }
    doubled := values.map { v => v * 2 }
    total := values.reduce(0) { (acc, v) => acc + v }

    println("${positive.len()} ${doubled.len()} ${total}")

    each_path(["/health", "/metrics"]) { path =>
        println("checking ${path}")
    }
}

Errors at a call site

Calling a fallible function produces a Result[T, E]. The caller must store, match, convert, or propagate it.

error CfgError { Missing }

fn read(key: string): string ! CfgError {
    if key == "" {
        error Missing
    }
    return "value-of-${key}"
}

fn build(): string ! CfgError {
    host := read("host")?
    port := read("port")?
    return "${host}:${port}"
}

fn main(): void {
    println(build().unwrap_or("unconfigured"))

    direct := read("") catch {
        err => {
            println("missing key")
            return
        }
    }
    println(direct)
}

? applies after the call and exits the enclosing fallible function. Argument evaluation that already happened is not rolled back when a call returns Err; compensation stays an explicit policy. See Errors.

Effects at a call site

A call inherits the callee’s effect row, so ordinary call syntax may allocate, suspend, spawn, or exit through the error channel.

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

fn parallel(a: int, b: int): int {
    left := spawn { slow(a) }
    right := spawn { slow(b) }
    return left.await() + right.await()
}

fn main(): void {
    println("${parallel(2, 3)}")
}

An enclosing declared using row must permit every inherited effect; without one, the compiler infers the union transitively. If a call suspends, values that are live afterwards are kept in the generated continuation — a consequence of the callee’s signature, not of any special call punctuation.

A complete example

error FetchError { Offline, NotFound }

struct Client {
    base: string
    retries: int

    fn get(self, path: string): string ! FetchError {
        if path == "" {
            error NotFound
        }
        if self.retries == 0 {
            error Offline
        }
        return "${self.base}${path}"
    }
}

impl Client {
    fn local(): Client => Client { base: "http://localhost", retries: 3 }
}

fn with_retries(c: Client, n: int): Client => Client { base: c.base, retries: n }

fn each_path(paths: []string, body: fn(string) -> void): void {
    for p in paths {
        body(p)
    }
}

fn main(): void {
    client := with_retries(Client.local(), 2)

    each_path(["/health", "/metrics", ""]) { path =>
        body := client.get(path) catch {
            err => {
                println("${path} -> failed")
                return
            }
        }
        println("${path} -> ${body}")
    }
}

Every call form appears here: a static function through the type (Client.local()), a free function (with_retries), a method on a value (client.get), a trailing-closure call (each_path(...) { ... }), a call to a function value (body(p)), and a fallible call discharged with catch.

Diagnostics

Call diagnostics name the contract that failed, and each one has its own fix:

Diagnostic Cause Fix
unresolved name / no method on type The callee or method does not exist for that type Import it, or add the method
takes N arguments, but M supplied Wrong arity Match the signature; defaults only fill a trailing suffix
type mismatch in argument position Incompatible argument Convert explicitly, or change the parameter
must be a mutable place Immutable binding passed to a mut parameter Declare the binding mut
unsatisfied bound The type argument does not implement a required trait Add the implementation or @derive it
unhandled Result A fallible call whose error channel was ignored ?, catch, or unwrap_or

Adding a cast does not repair missing mutability, a missing error boundary, or an unsatisfied bound — fix the dimension the diagnostic names.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close