Skip to content

Closures

Closure syntax, contextual parameters, captures, mutation, escape, and trailing calls.

Updated View as Markdown

A closure is an unnamed function value created with =>.

double := value => value * 2
add := (a: int, b: int) => a + b
constant := () => 42

Closures can be passed to functions, stored in bindings, returned when the inferred type permits it, and used by collection operations.

Parameters

A one-parameter closure may omit parentheses.

square := value => value * value

Use parentheses for zero or multiple parameters.

nothing := () => 0
sum := (left, right) => left + right

Parameter types may be inferred from the expected function type.

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

answer := apply(21, value => value * 2)

Write annotations when the context does not determine the parameter types.

compare := (left: int, right: int) => left < right

Closure parameters do not support declaration defaults or variadic forms.

Bodies

The expression after => is the closure’s result.

absolute := value => if value < 0 { -value } else { value }

A brace block supports statements and early returns from the closure itself.

normalize := (value: int) => {
    if value < 0 {
        return -value
    }
    value
}

The semantic checker unifies the body with the expected function result. A fallible closure can propagate errors when its expected function type carries the corresponding error contract.

Trailing calls

A final closure argument can be written after the ordinary argument list.

positive := values.filter { value => value > 0 }
retried := retry(3) { () => perform_work() }

The braces belong to the trailing closure argument, not to the call’s result. This form is useful for callbacks with multi-line bodies.

items.for_each {
    item => {
        println(item)
    }
}

Captures

A closure may use names from its surrounding lexical scope.

factor := 3
scale := value => value * factor

The compiler determines the capture set. There is no capture list in source. Immutable scalar captures behave as captured values; reference-shaped values retain the storage needed by the closure.

prefix := "item"
format := value => "$prefix: $value"

Captured values remain alive for as long as an escaping closure needs them. Programmers do not manually allocate or release a closure environment.

Mutable captures

A closure can observe and update a surrounding mutable binding when the checker can establish one shared mutable place.

fn make_counter(): fn() -> int {
    mut count := 0
    return () => {
        count += 1
        count
    }
}

Closures from the same capture group that share a mutable local observe the same updates. The compiler moves escaping mutable captures into managed environment storage; this is not a user-visible Cell type.

Mutation remains subject to normal exclusivity and lifetime rules. Capturing a name does not make an immutable binding mutable.

Escape

A closure escapes when it can outlive the activation that created it—for example, when it is returned, stored in a longer-lived value, or passed across an unknown call boundary.

fn make_adder(amount: int): fn(int) -> int {
    return value => value + amount
}

The compiler allocates a typed closure environment for an escaping closure. Non-escaping closures may be inlined or lowered to direct helper calls. These choices do not change source semantics.

Suspension

A closure can cross a suspension point only when all reachable captured state has a resumable representation. The compiler keeps escaping environments in arena-managed storage and includes the closure’s effects in call analysis.

Passing a closure to a suspend-capable function is conservatively treated as an escaping boundary unless analysis proves otherwise. This is why a closure that is immediately invoked may optimize differently from one stored in a task or stream callback.

Function compatibility

Named functions and closures can both satisfy compatible function types.

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

named: fn(int) -> int = twice
anonymous: fn(int) -> int = value => value * 2

Parameter types, success types, error types, and declared effects all participate in compatibility. A closure is not converted through an untyped callback representation.

Collection use

Lists, slices, maps, sets, strings, and iterators expose higher-order methods. Common examples include mapping, filtering, searching, folding, and iteration.

even := values.filter { value => value % 2 == 0 }
doubled := values.map { value => value * 2 }

Exact method names and mutability requirements are documented in the Standard Library.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close