Skip to content

Guide 11 of 53

Functions

Functions, closures, generators and async functions.

On this page

Basic Functions

fn greet(name: str) -> str {
    "Hello, {name}!"
}

fn add(a: int, b: int) -> int {
    a + b   // implicit return — last expression is the return value
}

Expression bodies

A function or method with one expression may use =:

fn double(x: int) -> int = x * 2
fn absolute(x: int) -> int = if x < 0 { -x } else { x }
fn identity<T>(value: T) -> T = value

This has the same return value as a braced body whose last expression has no semicolon. An optional semicolon after the expression separates declarations; it does not discard the function's result. A multiline expression and a block expression also work. Use a braced body when writing several statements.

Return inference, defaults, generics, effects and cleanup follow the ordinary function rules. A Result function still constructs its successful value:

fn checked_double(value: int) -> Result<int, str> = .Ok(read_positive(value)? * 2)

See the runnable example.

Conditions on entry and return

Use requires for an input condition and ensures for a condition on the normal return value. The name result refers to that value in ensures:

fn remaining_stock(available: int, requested: int) -> int
    requires available >= 0
    requires requested >= 0 && requested <= available
    ensures result >= 0 && result + requested == available
{
    if requested == 0 { return available }
    available - requested
}

assert(remaining_stock(12, 5) == 7)

Every condition must be Boolean. They run once per call, in header order, with all requires clauses before ensures. A false condition raises an error naming the function, its authored condition and source location.

Tail expressions, explicit return and propagation with ? reach ensures after the body's cleanup. Result::Err is a normal return; panics and failed tasks skip postconditions. In an async fn, the postcondition checks the completed value before the Future completes. Conditions can read parameters and outer bindings, but cannot read variables declared inside the body. Conditions cannot return or propagate out of the checked function. Use an inner callback or attempt when a condition needs its own control flow. An explicit -> void keeps its unit return; result is nil for its checks. Methods and trait default bodies support contracts. Contracts on declarations without a body and on generators are rejected. Async checked functions are unsupported at comptime.

See the runnable example.

Explicit Return

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

No Return Type

Functions that don't return a value omit the -> Type:

fn say_hello(name: str) {
    print("Hello, {name}!")
}

Parameters

Type Annotations

fn process(x: int, y: f64, name: str) -> str {
    "{name}: {x}, {y}"
}

Named Arguments

fn create_user(name: str, age: int, email: str) -> User {
    User { name, age, email }
}

// Call with named arguments
create_user(name: "Alice", age: 30, email: "alice@example.com")

When the local value has the same name as the argument label, write name::

let name = "Alice"
let age = 30
let email = "alice@example.com"
create_user(email:, name:, age:) // email: email, name: name, age: age

Shorthand works in function and method argument lists and can be mixed with explicit values. It reads the variable in the caller's lexical scope; it does not request a default value. Reordering, defaults and validation follow the ordinary named-argument rules. Direct decorator arguments still require an explicit value.

Function types can also expose a named-call interface. This is useful for callbacks and Verniz components:

fn TodoList(items: [Todo], row: fn(item: Todo) -> View) -> View {
    <ul>{for todo in items { <row item={todo} /> }}</ul>
}

The label is erased after named arguments are reordered, so it does not change type compatibility: a function declared as fn todo_row(todo: Todo) -> View can be passed where fn(item: Todo) -> View is expected. The older fn(Todo) -> View spelling remains valid and positional-only. A function type must use names for every parameter or for none of them.

Method calls accept named arguments too. Named arguments are resolved before types are known, so the method name selects the signature: when exactly one impl in the program (or in the standard library) declares a method of that name whose parameters cover every label used, the call is reordered like a free function and omitted defaults are filled in. If several declarations with different parameter lists match, the call is ambiguous and reported as TE131; call it positionally. A method nobody declares is left untouched.

impl Box {
    fn scale(self, by: int = 1, plus: int = 0) -> int { self.n * by + plus }
}

b.scale(plus: 4, by: 2)   // by = 2, plus = 4
b.scale(plus: 1)          // by keeps its default

Public labels and internal names

Use two names when the call reads better with a label that differs from the name used in the body:

fn distance(from start: int, to finish: int) -> int = finish - start
distance(from: 2, to: 8)

The first name is the public argument label; the second is the local parameter. distance(2, 8) remains valid. Calling with start: is an unknown label. A single-name parameter retains its existing public label and local binding. Defaults and captures use the internal name, while named puns read the caller:

fn advance(from start: int, to finish: int = start + 2) -> int = finish - start
let from = 4
advance(from:) // the caller supplies from; finish defaults using start

Methods, generic functions and labelled callback types follow the same rules. A named trailing block selects the public label: dialog() confirm { save() } can call a parameter declared as confirm action: fn() -> int. An explicitly labelled function type supplies the callable value's public interface. Renaming an explicit public label updates its call sites; renaming the internal name updates its body and default references.

See the runnable example.

Default Values

Parameters can have explicit defaults, and name?: T is the concise form for an optional parameter whose default is nil:

fn Card(children?: fn() -> View) -> View {
    <section>{children?.()}</section>
}

The declarator makes the parameter itself optional. This distinction matters for function types: callback?: fn() -> View is an optional function, while callback: fn() -> View? is a required function with an optional return.

callback?.(args) evaluates the callback once. When it is nil, it returns nil without evaluating args; otherwise it calls the value directly. The spelling callback?() keeps the existing ? propagation semantics and is not an optional call.

Lambdas / Closures

Lambdas use the |params| expr syntax:

// Single expression
let double = |x| x * 2
let add = |a, b| a + b

// With type annotations
let square = |x: int| -> int { x * x }

// Multi-line body
let process = |x: int| -> int {
    let y = x * 2
    y + 1
}

Short Lambdas

A proved unary callback argument can omit the parameter declaration:

let adults = users.filter(_.active && _.age >= 18)
let amounts = items.map(_.price * _.quantity)
let adjusted = items.map(_.price + adjustment)
let doubled = numbers.map(_ * 2)
let unchanged = numbers.map(_)
let names = users.map(format_name(_.name))
let lengths = groups.map(_.map(_ + 1).len())

Every _ in the callback's expression refers to the same parameter, including ordinary value arguments passed to nested calls. Normal local variables such as adjustment are captured. A nested callback position establishes its own parameter: the inner _ + 1 above receives one group element, while the outer _ receives the group. Explicit lambdas, blocks, collections, control flow and pipes keep their authored boundaries.

Scalar and identity shorthand require an actual expected unary callable type; an ordinary value parameter or unresolved dynamic call does not become a callback merely because its argument contains _. Access-based shorthand remains available for existing untyped callback calls. Pipe slots and discard bindings keep their existing meaning. Use an explicit lambda to name multiple callback parameters or write a block body.

Closures Capture Variables

let multiplier = 3
let multiply = |x| x * multiplier  // captures `multiplier`
print(multiply(5))  // 15

Named callback blocks

Several callbacks can follow a call, using their parameter names:

fn dialog(title: str, content: fn() -> str, confirm: fn() -> bool) -> str {
    let message = content()
    if confirm() { "{title}: {message}" } else { "{title}: cancelled" }
}

let answer = dialog(title: "Delete?") content {
    "This removes one file."
} confirm {
    true
}

This supplies content: || { ... } and confirm: || { ... }. Each body runs when dialog invokes that callback. A block with parameters writes them inside its braces: transform(3) next { |value| value * 2 }.

Labels refer to callable parameters of the actual target; ordinary named argument rules still reject unknown, duplicate or already supplied labels. The call evaluates the target and creates arguments in the order written, even when the labels appear in a different order from the declaration. Captures, optional callbacks, return types, effects and cleanup retain their ordinary lambda rules.

Start each label on the line containing the preceding ) or }; the body can span several lines. A semicolon or a new line before the label ends attachment. Bare Point { ... } keeps its struct literal meaning. Words used as operators, such as with, as, is and in, keep their existing meaning; pass callbacks with those labels inside parentheses. Named callbacks retain ordinary lambda behavior, including a parameter named children; their spelling does not enable view collection.

See the runnable example.

Higher-Order Functions

Functions can take functions as parameters and return functions:

fn apply(f: fn(int) -> int, x: int) -> int {
    f(x)
}

fn make_adder(n: int) -> fn(int) -> int {
    |x| x + n
}

let add5 = make_adder(5)
print(add5(10))  // 15

Methods

Methods are defined inside impl blocks. See Data Structures for details.

struct Circle {
    radius: f64,
}

impl Circle {
    fn area(self) -> f64 {
        3.14159 * self.radius * self.radius
    }

    fn new(radius: f64) -> Circle {
        Circle { radius }
    }
}

let c = Circle { radius: 5.0 }
print(c.area())

Decorators

Functions can be annotated with decorators:

@test
fn test_addition() {
    assert_eq(2 + 2, 4, "basic addition")
}

@memoize
fn fibonacci(n: int) -> int {
    if n <= 1 { n } else { fibonacci(n - 1) + fibonacci(n - 2) }
}

@deprecated("use new_function() instead")
fn old_function() {
    // ...
}

See Decorators for the full list.

DOCS / FEEDBACK

Did this page leave a question?

Tell us where the explanation lost you. Documentation is part of the language experience.

Global index

Find your way through Zolo

Try an idea

Start here

9 results

9 results

enespt-br