Skip to main content
v0.11.0

Tutorial: Closures and Capturing

Closures are anonymous functions that can be stored in variables, passed to other functions, and returned from functions. They are a natural fit for small bits of behaviour that need to travel with data.

A basic closure​

Closures use the (...) -> ... { ... } form:

fun main() {
let double = (x: i64) -> i64 { return x * 2; };
println(double(21)); // 42
}

The closure has the function type (i64) -> i64.

tip

Useful Rule

If a parameter expects (i64) -> i64, you can pass either a named function or a closure with that shape.

Passing closures around​

Closures and named functions can be used through the same function type:

fun apply_twice(f: (i64) -> i64, x: i64) -> i64 {
return f(f(x));
}

fun main() {
let bump = (x: i64) -> i64 { return x + 1; };
println(apply_twice(bump, 10)); // 12
}

This is the basic pattern behind callbacks and small reusable transforms.

A useful mental model is: a closure is a function value that may carry some captured context with it.

Capturing values from the outside​

A closure can use bindings from the surrounding scope:

fun main() {
let suffix = "!";
let excite = (word: String) -> String {
return word + suffix;
};

println(excite("Metel")); // Metel!
}

The closure body refers to suffix even though suffix is not a parameter. That is a capture.

note

Capture Means “Use an Outer Binding”

If the closure body mentions a binding declared outside the closure, that binding becomes part of the closure's environment.

Shared mutable state uses references​

Closures capture by value. If you want shared mutable state across closures, make that sharing explicit with a mutable reference:

fun main() {
var count = 0;
let r: &var i64 = &var count;

let inc = () -> () {
r += 1;
};

let get = () -> i64 {
r
};

inc();
inc();
println(get()); // 2
}

This keeps shared mutation explicit. The closures do not magically share count; they share the reference r.

caution

Shared Mutation Must Be Explicit

If multiple closures need to mutate the same value, introduce a mutable reference on purpose. Ordinary closure capture is not the mechanism for implicit shared mutable state.

Returning a closure​

Closures can be returned like any other value:

fun make_adder(base: i64) -> (i64) -> i64 {
return (x: i64) -> i64 {
return x + base;
};
}

fun main() {
let add_five = make_adder(5);
println(add_five(9)); // 14
}

The returned closure keeps access to base, so the behaviour stays tied to the value captured when it was created.

Returning a closure lets you build small configurable behaviours without introducing a named struct or enum just to hold one captured value.

A practical example​

Here a closure tracks how many items matched a filter:

fun main() {
let values = [1, 2, 3, 4, 5, 6];
var matched = 0;
let matched_ref: &var i64 = &var matched;

let is_even = (x: i64) -> boolean {
if (x % 2 == 0) {
matched_ref += 1;
return true;
}
return false;
};

for (let value in values) {
if (is_even(value)) {
println("${value} is even");
}
}

let matched_total: i64 = matched_ref;
println("matched ${matched_total} values");
}

The closure both computes an answer and updates shared state, but the shared state is explicit because it flows through matched_ref.

For the public language today, the main rules to remember are:

  • closures use (...) -> ... { ... }
  • they can capture outer bindings
  • shared mutable state across closures should be explicit and reference-based

What you learned​

  • Closures are anonymous functions with ordinary function types.
  • They can be passed, stored, and returned.
  • A closure captures bindings from the surrounding scope automatically.
  • Shared mutable closure state is explicit: use &var references when multiple closures must mutate the same value.

Next: Control Flow and Errors — loops, optional values, and error handling.