Closures

#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]}");
}