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 * 2This 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:
() -> 42This 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 + bThis 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) // 7Block 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 bodyVariable 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 WorldThe 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()) // 3This 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)) // 10Storing 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)) // 7This 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)) // 6Common 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)) // 15The 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) // 7Filtering 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 parametersBlock bodies
x -> { return x * 2 } // single-param block
() -> { return 42 } // zero-param block
(a, b) -> { return a + b } // multi-param blockFunction 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 boolStoring 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