#warn Dead assignment — base is overwritten before being read @TITLE: Capturing variables from the surrounding scope A closure is a lambda that reads variables from the function scope in which it is written. No explicit capture list is needed — the compiler detects which outer variables the lambda body uses and packages them automatically.
Integer capture
A lambda may read any integer variable in scope. Store the lambda, then call it by name.
Multiple integer captures
A lambda can read more than one outer variable at once. All are captured at the moment the lambda is written.
Text capture
Text values are captured by deep-copy, so they are independent of the original variable after capture.
Capture timing
Loft captures variables at the moment the lambda is written (definition time), not when it is called. If a variable changes after the lambda is written, the lambda still sees the original value.
Cross-scope closures
A function can return a capturing lambda to the caller. The captured values travel with the lambda — no dangling references.
Closures with higher-order functions
A capturing closure stored in a variable can be called directly.
Non-capturing lambdas with higher-order functions
Lambdas that use only their own parameters (no capture) also work fine.
fn make_adder(base_val: integer) -> fn(integer) -> integer {
fn(n: integer) -> integer { base_val + n }
}
fn main() {
Integer capture
offset = 100;
shift = fn(x: integer) -> integer { x + offset };
assert(shift(5) == 105, "shift(5): {shift(5)}");
assert(shift(-3) == 97, "shift(-3): {shift(-3)}");
Multiple integer captures
lo = 10;
hi = 20;
clamp_val = fn(x: integer) -> integer {
if x < lo { lo } else if x > hi { hi } else { x }
};
assert(clamp_val(5) == 10, "clamp below: {clamp_val(5)}");
assert(clamp_val(15) == 15, "clamp mid: {clamp_val(15)}");
assert(clamp_val(25) == 20, "clamp above: {clamp_val(25)}");
Text capture
greeting = "Hello";
make_msg = fn(name: text) -> text { "{greeting}, {name}!" };
assert(make_msg("World") == "Hello, World!", "greeting capture");
assert(make_msg("Loft") == "Hello, Loft!", "greeting capture 2");
Capture timing
Closures capture at definition time. base is 10 when the lambda is written; reassigning base to 20 afterwards does not affect the captured value.
base = 10;
add_base = fn(n: integer) -> integer { base + n };
base = 20;
assert(add_base(5) == 15, "sees base=10 at definition time: {add_base(5)}");
Cross-scope closures
make_adder returns a lambda that captured base_val from its parameter.
add10 = make_adder(10);
add100 = make_adder(100);
assert(add10(5) == 15, "add10(5): {add10(5)}");
assert(add100(5) == 105, "add100(5): {add100(5)}");
Closures with higher-order functions
factor = 3;
scale = fn(x: integer) -> integer { x * factor };
assert(scale(5) == 15, "scale(5): {scale(5)}");
assert(scale(10) == 30, "scale(10): {scale(10)}");
Non-capturing lambdas with higher-order functions
No capture needed here — the lambda uses only its own parameter.
nums = [1, 2, 3, 4, 5];
doubled = map(nums, |x| { x * 2 });
assert(doubled[0] == 2, "doubled[0]: {doubled[0]}");
assert(doubled[4] == 10, "doubled[4]: {doubled[4]}");
evens = filter(nums, |x| { x % 2 == 0 });
assert(evens[0] == 2, "evens[0]: {evens[0]}");
assert(evens[1] == 4, "evens[1]: {evens[1]}");
}