Lambdas and Closures
On this page
A lambda is an unnamed function, written as |params| expr or
|params| { block }. Lambdas are first-class values: they can be
assigned to variables, passed as arguments, and returned from functions:
Short form, typed form, block form, and a parameter-less lambda (thunk).
// Feature: Anonymous functions (lambdas)
// Syntax: `|a, b| expr` or `|a: T| { ... }` (block body)
// When to use: short callbacks, inline function values,
// functional composition without naming each step.
// Short form: body is a single expression.
let double = |x| x * 2
let add = |a, b| a + b
print(double(5)) // 10
print(add(3, 4)) // 7
// Parameters can be explicitly typed.
let mul: fn(int, int) -> int = |a: int, b: int| a * b
print(mul(6, 7)) // 42
// Block form — multiple lines, requires `return`.
let process = |x: int| {
let doubled = x * 2
let plus_one = doubled + 1
return plus_one
}
print(process(10)) // 21
// Lambdas are values: pass them as arguments.
fn apply(f: fn(int) -> int, x: int) -> int {
return f(x)
}
print(apply(|n| n * n, 9)) // 81
// Lambda without parameters — useful for a "thunk" (lazy value).
let now = || 42
print(now()) // 42
When a lambda is created inside a function, it captures the variables from the outer scope by their bound value — this turns it into a closure. Each call to the function manufactures an independent closure:
make_adder, make_formatter — each closure carries its own immutable captured state.
// Feature: Closures — lambdas that capture lexical scope
// Syntax: `|args| body` inside an fn — captures `let`
// and `let mut` that are visible.
// When to use: remember configuration, create functions with
// embedded state, close over local helpers.
use std::Array
// Capturing a constant from the outer scope.
fn make_adder(n: int) {
return |x| x + n // captures `n`
}
let add5 = make_adder(5)
let add10 = make_adder(10)
print(add5(3)) // 8
print(add10(3)) // 13
// Each call creates an independent closure.
let addA = make_adder(100)
let addB = make_adder(200)
print(addA(1)) // 101
print(addB(1)) // 201
// Closures can capture multiple variables.
fn make_formatter(prefix: str, suffix: str) {
return |s| prefix + s + suffix
}
let bracket = make_formatter("[", "]")
let bold = make_formatter("**", "**")
print(bracket("ok")) // [ok]
print(bold("warning")) // **warning**
// Closures naturally combine with higher-order.
let nums = [1, 2, 3, 4, 5]
let threshold = 3
let big = nums.filter(|x| x > threshold)
print(big) // [4, 5]
Zolo also lets the last lambda argument leave the parentheses — a
trailing lambda. f(a) { |x| body } reads like a built-in block and
desugars to f(a, |x| body); a block with no |params| becomes a
zero-arg thunk, so f(a) { body } desugars to f(a, || body):
each's block carries |x|; retry's { 42 } has no params, so it becomes a thunk.
// Feature: Trailing lambda
// Syntax: `f(a) { |x| body }` desugars to `f(a, |x| body)`; a block
// with no `|params|` desugars to a zero-arg thunk `f(a, || body)`.
// When to use: callback-shaped calls (each, retry, with-resource)
// read like built-in control flow instead of a lambda squeezed
// into the argument list.
fn each(xs: [int], f: fn(int) -> Any) {
for x in xs { f(x) }
}
// Block with `|params|` after the closing `)` — same as
// `each([1, 2, 3], |x| print(x))`.
each([1, 2, 3]) { |x| print(x) }
// 1
// 2
// 3
fn retry(times: int, f: fn() -> int) -> int {
var last = 0
var i = 0
while i < times { last = f(); i += 1 }
return last
}
// Thunk form — no `|params|`, desugars to `retry(2, || 42)`.
let r = retry(2) { 42 }
print(r) // 42
Challenge
Write make_multiplier(factor: int) that returns a lambda multiplying
by factor. Create double and triple and apply them to [1, 2, 3, 4, 5] with .map().