7.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.
7.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
Top-level functions accept the same arrow form — see Single-expression body.
7.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.
7.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") }
}
7.1.14.4. Storing lambdas
A lambda value is a fat pointer to a heap-allocated capture frame.
= copies the pointer (both bindings now alias the same capture frame),
<- moves (source becomes null):
var a <- @() { print("hello\n") }
var b = a // b and a now alias the same lambda
var c <- a // c takes ownership, a becomes null
c()
Because copies share the capture frame, delete lam requires
unsafe { ... } — the caller asserts no other live copy exists. Under
the default GC the capture frame is freed automatically when no copy
remains reachable.
Lambdas 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)
}
unsafe { delete callbacks; } // array<lambda<...>> inherits the unsafe-delete rule
Blocks cannot be stored in arrays or variables — they live on the stack and are only valid as function arguments.
7.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.
7.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)
7.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.
7.1.14.7. Lambda vs block vs function pointer
Feature |
|
|
|
|---|---|---|---|
Allocation |
None |
Heap |
Stack |
Capture |
None |
By copy (default) |
By reference |
Storable |
Yes |
Yes (copy aliases capture) |
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