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 zeroed

  • capture(clone(var)) — deep copy; source unchanged

  • capture(ref(var)) — by reference (requires unsafe)

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:

  1. Capture — outer variables are copied/moved/cloned into the struct

  2. Invoke — the lambda body runs (may be called many times)

  3. Destroy — when the lambda is deleted or garbage collected: a) The finally{} block runs (user cleanup code) b) Captured fields are finalized (compiler-generated delete) 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 doNotDelete

  • The 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

function<> (@@)

lambda<> (@)

block<> ($)

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