Skip to content

06 · Functions

Lambdas and Closures

Anonymous functions with |args| expr and lexical scope captures.

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).

02-anonymous-lambdas.zolo
Playground
// 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.

07-closures.zolo
Playground
// 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.

13-trailing-lambda.zolo
Playground
// 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

A short lambda can combine several accesses to one implicit parameter: users.filter(_.active && _.age >= 18). Captured local values work normally. A proved unary callback also accepts _ * 2 and ordinary nested value calls such as format_name(_.name). Nested callback slots bind their own parameter; explicit lambdas and blocks establish separate scopes.

Repeated rooted _ accesses share a parameter; predicates and arithmetic transforms retain contextual types.

19-short-lambda-predicates.zolo
Playground
// Short lambdas share one parameter across the argument's rooted accesses.

struct User { name: str, age: int, active: bool }
struct Line { price: int, quantity: int }

fn main() {
    let users = [
        User { name: "Ada", age: 36, active: true },
        User { name: "Bo", age: 17, active: true },
        User { name: "Cy", age: 24, active: false },
    ]
    print(users.filter(_.active && _.age >= 18).map(_.name))
    // expected: [Ada]


    let lines = [Line { price: 3, quantity: 4 }, Line { price: 5, quantity: 2 }]
    print(lines.map(_.price * _.quantity))
    // expected: [12, 10]


    let adjustment = 2
    print(lines.map(_.price + adjustment))
    // expected: [5, 7]

}

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().

Fixing arguments once

A partial application keeps fixed arguments at construction time. Each underscore becomes a separate input of the new function.

06-partial-application.zolo
Playground
// Fixed arguments are acquired once; each underscore is an input of the new function.

var acquisitions = 0
fn initial() -> int { acquisitions += 1; 10 }
fn add(base: int, amount: int) -> int { base + amount }
let add_initial = partial add(initial(), _)
print(add_initial(5)) // expect: 15

print(add_initial(8)) // expect: 18

print(acquisitions) // expect: 1

fn subtract(left: int, right: int) -> int { left - right }
let remaining = partial subtract(right: _, left: 20)
print(remaining(3)) // expect: 17

Choosing captured values

An explicit capture header chooses values before the closure is created. Updating the outer binding afterward does not change the captured alias.

14-explicit-captures.zolo
Playground
// The closure uses the price chosen at construction, even after it changes.

var unit_price = 5
let quote = capture { price: unit_price } |quantity: int| price * quantity
unit_price = 9
print(quote(3)) // expect: 15

print(unit_price) // expect: 9

Global index

Find your way through Zolo

Try an idea

Start here

9 results

9 results

enespt-br