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