Skip to Content

Lambdas and Closures

A lambda is an anonymous function value. The syntax comes in a short family and a complete family, and once it captures outer local variables it becomes a closure. Reach for one when you need a callback or a small one-off function without inventing a name.

The Two Forms

(a, b) => expr // short: expression body (x: T) => { stmt; } // short: block body func(x: T) -> U => expr // complete: func keyword + return annotation
  • The parameter list always takes parentheses; there is no single-parameter parenthesis-free form;
  • parameter types may be annotated or inferred, and a block body’s return type is inferred from its return;
  • the complete form can carry clauses: func(x: T) -> U uses IO => expr, and failable.
val add = (a: int, b: int) => a + b; val classify = (n: int) => { if n >= 0 { return "non-negative"; } return "negative"; }; val hello = () => "hello"; print(add(2, 3), classify(-1), hello());
5 negative hello

Calling immediately after definition is also legal: wrap the lambda in parentheses and follow with the arguments:

print(((n: int) => n * 2)(21));
42

Function Types

A lambda’s type is a function type, writable two equivalent ways:

(int, int) -> int // short form func(int, int) -> int // explicit form

All of them work in annotation position, parameter position, and return position. Higher-order functions put function types in their signatures:

func Apply(f: func(int) -> int, n: int) -> int => f(n); print(Apply((x) => x * 2, 5));
10

Apply((x: int) => x + 1, 9) passes a lambda straight in as an argument.

Closure Capture

A lambda body can read outer local variables directly, and it captures the variable itself, not its value at that moment: later changes outside are visible inside, and writes inside the lambda outlive the call:

func MakeCounter() -> func() -> int { var n = 0; return () => { n += 1; return n; }; } val tick = MakeCounter(); val tick2 = MakeCounter(); print(tick(), tick(), tick(), tick2());
1 2 3 1

Each call to tick increments its n by 1, yielding 1, 2, 3 across three calls. tick2 comes from a separate MakeCounter() call with its own n, starting from 1. Closures make small stateful behaviors possible without writing a class first.

Versus Named Function Values

A named function’s name also materializes into a function value in value position, but its identity rules differ from a lambda’s:

  • Same declaration, same value (a singleton): val a = On; val b = On; are one and the same function value;
  • each lambda evaluation creates a new instance: writing the same lambda expression twice yields two unequal values, each with its own closure environment.

Where deduplication by identity matters (for example, matching an unsubscribe against an event subscription), use a named function — see the Events and Reactivity part. == judges by instance identity: named singletons are identical, separately evaluated lambdas are never equal — see Equality and Hashing.

failable lambda

A complete lambda can carry a fail clause; fail E may sit before or after ->. fail statements in the body collect into the function value’s own failure contract:

error Bad(n: int); val risky = func(x: int) fail Bad -> int { if x < 0 { fail new Bad(x); } return x; }; print(try? risky(5)); print(try? risky(-1));
5 null

Calling it falls under the same consumption obligation: try? risky(5) gives 5, try? risky(-1) gives null. Passing a lambda with a wider fail set to a slot with a narrower contract is rejected at compile time.

Notes

  • async (x) => expr is an async lambda returning task<T> — see the Async part;
  • the rules for capturing self inside a lambda involve type members — see the Types part;
Last updated on October 11, 2026