Closures and Anonymous Functions

Closures and Anonymous Functions

Closures let you create functions on the fly — without giving them a name — and pass them around as values. They can capture variables from the surrounding scope, making them powerful building blocks for callbacks, factories, and higher-order patterns.

This page covers:

  • Anonymous function (lambda) syntax
  • Zero-parameter, single-parameter, and multi-parameter forms
  • Block bodies for multi-statement lambdas
  • Variable capture (closures)
  • Storing functions in variables
  • Common patterns

Lambda Syntax

Luma uses the arrow -> to define anonymous functions (lambdas). There are three forms depending on how many parameters you need.

Single parameter

The simplest form — no parentheses needed:

x -> x * 2

This creates a function that takes one argument and returns it doubled. You can use it anywhere a function value is expected:

doubled: [int] = [1, 2, 3].map(x -> x * 2)
print(doubled)   // [2, 4, 6]

Zero parameters

Use empty parentheses when the function takes no arguments:

() -> 42

This is useful for callbacks and deferred actions:

fn do_twice(action: () -> str) -> str {
    return action()
}

Multiple parameters

Wrap multiple parameters in parentheses:

(a, b) -> a + b

This creates a function that takes two arguments:

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

result: int = apply((a, b) -> a + b, 3, 4)
print(result)   // 7

Block Bodies

For lambdas that need more than a single expression, use a block body with { }:

x -> {
    doubled: int = x * 2
    return doubled + 1
}

Block bodies follow the same rules as function bodies — use return to produce a value.

This works with all parameter forms:

// Zero-param block
() -> {
    print("Starting...")
    return 42
}

// Single-param block
x -> {
    print("Got: ${x}")
    return x * 2
}

// Multi-param block
(a, b) -> {
    sum: int = a + b
    return sum * 2
}

Single-expression blocks are automatically simplified. These are equivalent:

x -> { x * 2 }    // block with one expression
x -> x * 2        // expression body

Variable Capture (Closures)

A lambda becomes a closure when it references variables from its enclosing scope. The lambda “captures” those variables and can use them later, even after the enclosing scope has ended.

Capturing a local variable

prefix: str = "Hello "
greeter: (str) -> str = name -> "${prefix}${name}"

print(greeter("Luma"))    // Hello Luma
print(greeter("World"))   // Hello World

The lambda name -> "${prefix}${name}" captures prefix from the outer scope. Every time greeter is called, it uses the captured value.

Capture by reference

Luma captures variables by reference. If a captured variable changes before the lambda is called, the lambda sees the new value:

count: int = 0
counter: () -> int = () -> {
    count = count + 1
    return count
}

print(counter())   // 1
print(counter())   // 2
print(counter())   // 3

This is important to understand — the lambda shares the variable with its enclosing scope, not a copy of it.

Storing Functions in Variables

Function types use the same arrow syntax: (ParamTypes...) -> ReturnType. You can store both lambdas and named functions in typed variables.

Storing a lambda

doubler: (int) -> int = x -> x * 2
print(doubler(5))   // 10

Storing a named function

Named functions can be assigned to variables with matching types:

fn add(a: int, b: int) -> int => a + b

op: (int, int) -> int = add
print(op(3, 4))   // 7

This is useful when you want to select behavior at runtime:

fn add(a: int, b: int) -> int => a + b
fn mul(a: int, b: int) -> int => a * b

op: (int, int) -> int = add
print(op(2, 3))   // 5

op = mul
print(op(2, 3))   // 6

Common Patterns

Factory pattern

A function that returns a configured lambda:

fn make_adder(a: int) -> (int) -> int {
    return b -> a + b
}

add5: (int) -> int = make_adder(5)
print(add5(3))    // 8
print(add5(10))   // 15

The returned lambda captures a from make_adder’s scope. Each call to make_adder produces a new closure with its own captured value.

Callback pattern

Passing a lambda to a function that calls it:

fn with_retry(action: () -> str, fallback: str) -> str {
    return action().or(fallback)
}

Higher-order functions

Functions that take functions as parameters and return new behavior:

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

result: int = apply_twice(x -> x + 1, 5)
print(result)   // 7

Filtering and transforming collections

Lambdas work naturally with collection methods:

nums: [int] = [1, 2, 3, 4, 5, 6]

evens: [int] = nums.filter(x -> x % 2 == 0)
print(evens)   // [2, 4, 6]

doubled: [int] = nums.map(x -> x * 2)
print(doubled)   // [2, 4, 6, 8, 10, 12]

Error handling with lambdas

Lambdas pair naturally with .or() for error handling:

fn divide(a: int, b: int) -> int {
    if b == 0 {
        error("Division by zero")
    }
    return a / b
}

// Expression lambda
x: int = divide(10, 0).or(err -> -1)

// Block lambda for logging
y: int = divide(10, 0).or(err -> {
    print("Failed: ${err}")
    return 0
})

Quick Reference

Lambda forms

x -> x * 2                 // single parameter
() -> 42                   // zero parameters
(a, b) -> a + b            // multiple parameters

Block bodies

x -> { return x * 2 }      // single-param block
() -> { return 42 }        // zero-param block
(a, b) -> { return a + b } // multi-param block

Function types

doubler: (int) -> int               // takes int, returns int
combiner: (int, int) -> int         // takes two ints, returns int
action: () -> str                   // takes nothing, returns str
predicate: (str) -> bool            // takes str, returns bool

Storing and calling

f: (int) -> int = x -> x + 1
print(f(5))                          // 6

fn named_add(a: int, b: int) -> int => a + b
op: (int, int) -> int = named_add
print(op(3, 4))                      // 7
Last updated on