How do closures and variable capture work in EK9?

← Getting Started · Ref: Q53

EK9 dynamic functions can capture variables from their enclosing scope, making them closures. But EK9's capture mechanism is fundamentally different from every other mainstream language in three ways.

1. EXPLICIT CAPTURE

Captured variables are listed explicitly in parentheses BEFORE the 'is' keyword:

  scaleFactor <- 10.0
  scaled <- (scaleFactor) is mathOperation as pure function
    result:=? x * scaleFactor + y

In Java, JavaScript, and Python, capture is AUTOMATIC and HIDDEN — any variable from the enclosing scope can be silently captured. This leads to accidental captures, unexpected dependencies, and hard-to-trace bugs. EK9 makes data flow visible.

2. CAPTURE BY VALUE

Captured variables are COPIED at creation time. The dynamic function gets its own independent copy. Changing the original variable after creation does NOT affect the captured value:

  factor <- 10.0
  scaled <- (factor) is mathOperation as pure function
    result:=? x * factor
  factor: 999.0
  scaled(3.0, 1.0)    //Still uses 10.0, not 999.0

This eliminates an entire class of bugs. In Java, closures capture by reference (must be effectively final as a workaround). In JavaScript and Python, closures capture by reference and CAN mutate — leading to the notorious Python late-binding bug and JavaScript loop-capture bugs.

3. NAMED CAPTURE PARAMETERS

Captures can have explicit values, making the function self-documenting:

  configured <- (factor: 2.0, offset: 5.0) is mathOperation as pure function
    result:=? x * factor + offset

The names and values are visible at the creation site. No need to look at surrounding code to understand what values the function uses.

MULTIPLE CAPTURES

Any number of variables can be captured:

  (min, max, label) is validator as function
    ...

CAPTURE CONSTRAINTS

- Variables must be named — literals cannot be captured directly
- Unused captures are a compiler error (E11018)
- Captured variables count toward coupling metrics
- The compiler tracks captures for quality enforcement

CAPTURE IN INLINE SYNTAX

Inline dynamic functions also support captures:

  biased <- (bias) is mathOperation as pure (result:=? x + y + bias)

See Q52 for dynamic function syntax. See Q57 for using captures to change class behaviour without subclassing. See Q58 for how captures enable stream pipeline integration. See Q59 for using captures in stream pipeline stages. See Q89 for stream pipeline basics. See Q115 for dynamic classes which use the same capture syntax. See Q235 for complete stream operations reference. See Q319 for unused closure capture detection (E11018). See Q601 for how dynamic functions with captures replace nested functions.

Example

defines module qa.closurecapture

  defines function

    mathOperation() as pure abstract
      ->
        x as Float
        y as Float
      <- result as Float?

  defines program

    CaptureDemo()
      stdout <- Stdout()

      // Explicit capture by value
      factor <- 10.0
      scaled <- (factor) is mathOperation as pure function
        result:=? x * factor + y

      stdout.println(`captured: ${scaled(3.0, 1.0)}`)

      // Changing original does NOT affect the captured copy
      factor: 999.0
      stdout.println(`factor is now: ${factor}`)
      stdout.println(`after change: ${scaled(3.0, 1.0)}`)

      // Named capture parameters — self-documenting
      configured <- (multiplier: 2.0, offset: 5.0) is mathOperation as pure function
        result:=? x * multiplier + offset

      stdout.println(`named: ${configured(4.0, 0.0)}`)

      // Multiple captures
      base <- 100.0
      adjustment <- 0.1
      adjusted <- (base, adjustment) is mathOperation as pure function
        result:=? base + (x * adjustment) + y

      stdout.println(`multiple: ${adjusted(50.0, 3.0)}`)

      // Inline capture
      bias <- 10.0
      biased <- (bias) is mathOperation as pure (result:=? x + y + bias)
      stdout.println(`inline: ${biased(1.0, 2.0)}`)

Common mistakes

E08010 — Capturing a variable that has not been defined yet in the program flow triggers E08010 — variable not defined. The variable 'base' is declared later in the program, so it cannot be captured at this point. See ek9 -h E08010 for details.

Incorrect:

scaled <- (factor, base) is mathOperation as pure function
        result:=? x * factor + y

Correct:

scaled <- (factor) is mathOperation as pure function
        result:=? x * factor + y

E08120 — In a pure dynamic function, mutating captured variables is forbidden. This triggers E08120 — mutating variables not allowed in pure scope. Captured values are immutable copies in pure functions. See ek9 -h E08120 for details.

Incorrect:

factor *= 2

Correct:

result:=? x * factor + y
Other ways to ask this
  • How does EK9 capture variables in dynamic functions?
  • What is the difference between capture by value and capture by reference?
  • How do named capture parameters work in EK9?

Coming from another language?

Java: closures capture by reference, must be effectively final (workaround for mutable state bugs), no explicit capture list, accidental capture of large objects causes memory leaks, lambda capture is hidden and automatic. Python: closures capture by reference with notorious late-binding bug (loop variable captured by reference not value), nonlocal keyword for mutation, no explicit capture list, no compile-time capture validation. JavaScript: closures capture by reference, mutable shared state is a major bug source, classic 'var in for loop' capture bug, no explicit capture list, no quality enforcement. Rust: closures can capture by reference (borrow) or by value (move), move keyword forces value capture, compiler enforces borrow rules but capture is still implicit, Fn/FnMut/FnOnce traits determine what captures can do. Go: closures capture by reference, mutable shared state, goroutine + closure capture bugs are common, no explicit capture list. Kotlin: closures capture by reference, CAN mutate captured vars (unlike Java), no explicit capture list. Swift: closures capture by reference by default, capture list [weak self, unowned x] for value capture, @escaping annotation required. EK9: capture is EXPLICIT (listed in parentheses), by VALUE (independent copy), named capture parameters for self-documenting code, unused captures are compiler errors, captures count toward quality metrics.

Keywords: closure, beginner, start, named, copy, reference, value, mutable, variable, intro, swift, first, function, migrate, capture, bug, explicit, state, independent, parameter