5.1.14. Lambdas and Closures
This tutorial covers lambda declarations with @(), capture modes
(copy, move, clone), call syntax, storing lambdas, and how lambdas
differ from blocks and function pointers.
5.1.14.1. Declaring a lambda
Lambdas are prefixed with @() and are heap-allocated:
var mul <- @(x : int) : int { return x * 3 }
print("{mul(10)}\n") // 30
Simplified syntax with =>:
var add <- @(a, b : int) : int => a + b
5.1.14.2. Call syntax
Lambda variables support call syntax — call them like regular functions:
var doubler <- @(x : int) : int => x * 2
print("{doubler(7)}\n") // 14
invoke() is the explicit alternative — same behavior:
print("{invoke(doubler, 7)}\n") // 14
Prefer call syntax; use invoke() when you need it.
5.1.14.3. Capture modes
By default, lambdas capture outer variables by copy:
var multiplier = 10
var fn <- @(x : int) : int { return x * multiplier }
// changing multiplier here does not affect fn
Use capture() for other modes:
capture(move(var))— transfers ownership; source is zeroedcapture(clone(var))— deep copy; source unchangedcapture(ref(var))— by reference (requiresunsafe)
var data : array<int>
data |> push(1)
unsafe {
var fn2 <- @capture(move(data)) () { print("{data}\n") }
}
5.1.14.4. Storing lambdas
Lambdas must be moved with <-, not copied:
var a <- @() { print("hello\n") }
var b <- a // a is now empty
b()
Lambdas cannot be copied, but they can be stored in arrays using
emplace, which moves the lambda into the container:
var callbacks : array<lambda<():void>>
var greet <- @() { print("hello from callback\n") }
callbacks |> emplace(greet) // greet is now empty
var farewell <- @() { print("goodbye from callback\n") }
callbacks |> emplace(farewell)
for (cb in callbacks) {
invoke(cb)
}
Blocks cannot be stored in arrays or variables — they live on the stack and are only valid as function arguments.
5.1.14.5. Stateful lambda factory
A function can create and return a lambda that captures state:
def make_counter() : lambda<():int> {
var count = 0
return <- @capture(clone(count)) () : int {
count += 1
return count
}
}
Each call creates an independent counter.
5.1.14.6. Lambda lifecycle and finally blocks
A lambda is a heap-allocated struct. The full lifecycle is:
Capture — outer variables are copied/moved/cloned into the struct
Invoke — the lambda body runs (may be called many times)
Destroy — when the lambda is deleted or garbage collected: a) The
finally{}block runs (user cleanup code) b) Captured fields are finalized (compiler-generateddelete) c) The struct memory is freed
Captured fields are automatically deleted on destruction unless:
The field was captured by reference (not owned)
The field was captured by move/clone with
doNotDeleteThe field type is POD (int, float — no cleanup needed)
5.1.14.6.1. Finally block
A lambda’s finally{} block runs once when the lambda is
destroyed — not after each invocation. This is different from
a block finally{}, which runs after every call:
var demo <- @() {
print("body\n")
} finally {
print("destroyed\n") // runs once, on deletion
}
demo() // prints "body"
demo() // prints "body"
unsafe { delete demo; } // prints "destroyed"
Use lambda finally{} for one-time destruction cleanup (releasing
resources, closing handles). For per-call cleanup, use scoped
variables or defer inside the lambda body.
5.1.14.7. Lambda vs block vs function pointer
Feature |
|
|
|
|---|---|---|---|
Allocation |
None |
Heap |
Stack |
Capture |
None |
By copy (default) |
By reference |
Storable |
Yes |
Yes (move only) |
No |
Returnable |
Yes |
Yes |
No |
A function<> parameter accepts only @@ values.
A lambda<> parameter accepts only @ values.
A block<> parameter accepts any of them (most flexible).
See Function Pointers for @@ and function<> details.
See also
Lambdas, Functions in the language reference.
Full source: tutorials/language/14_lambdas.das
Next tutorial: Iterators and Generators