How do I capture a value into a dynamic function/class without sharing mutations with the caller?
← Advanced Type System · Ref: Q1296
EK9 captures hold the POINTER value at capture time, not a deep copy. Each dynamic class/function instance has its own pointer slot — outer pointer rebinding (`outer := newValue`) does NOT affect the closure. But mutating the captured object via `+=`, `:=:`, `:~:`, or any other operator that modifies the pointed-to object IS visible to the caller, because both share the same object.
THREE PATTERNS:
Pattern 1 — Default capture (shared mutation):
original <- "Hello" sharedAppender <- (original) with trait of Appendable as class override append() -> extra as String original += extra //mutates the SHARED String default operator ? sharedAppender.append(" World") //outer 'original' is now "Hello World"
Pattern 2 — Isolation via `:=:` copy before capture:
master <- "Master" snapshot <- String() snapshot :=: master //copy contents into snapshot isolatedAppender <- (snapshot) with trait of Appendable as class ...mutates snapshot, not master
Pattern 3 — Isolation via copy constructor:
base <- "Base" copied <- String(base) //String's copy constructor copyAppender <- (copied) with trait of Appendable as class ...mutates copied, not base
IMPORTANT — `:=:` IS NOT ALWAYS DEEP
The `default operator :=:` on records does a SHALLOW copy (copies field references). For deep copy of nested mutable state, the type must implement its own `:=:` that recursively calls `:=:` on each field. See operators.html for the deep-vs-shallow distinction. The same caveat applies to copy constructors — whether they deep-copy or share references is up to the constructor's implementation.
This is the same issue Java has with reference types — EK9's advantage is that the operators (`:=` rebind vs `:=:` copy vs `:~:` merge) make the intent grammatically explicit instead of per-API guessing.
See Q1125 for dynamic function basics. See Q1126 for dynamic class basics. See Q1128 for dynamic class as argument.
Example
defines module qa.advancedtypes.dynamiccaptureisolation defines trait Appendable append() as abstract -> extra as String override operator ? as pure <- rtn as Boolean: true defines program DynamicCaptureIsolationDemo() stdout <- Stdout() //--- Pattern 1: Default capture — mutation is SHARED with caller --- //Captures hold pointer values; mutating the captured object affects the outer scope original <- "Hello" sharedAppender <- (original) with trait of Appendable as class override append() -> extra as String original += extra default operator ? stdout.println(original) sharedAppender.append(" World") stdout.println(original) //--- Pattern 2: Snapshot via :=: copy operator BEFORE capture --- //Take a copy first, then capture the copy — outer is isolated master <- "Master" snapshot <- String() snapshot :=: master isolatedAppender <- (snapshot) with trait of Appendable as class override append() -> extra as String snapshot += extra default operator ? isolatedAppender.append(" Backup") stdout.println(master) stdout.println(snapshot) //--- Pattern 3: Snapshot via copy constructor --- //Construct a new instance from the original — same isolation, different idiom base <- "Base" copied <- String(base) copyAppender <- (copied) with trait of Appendable as class override append() -> extra as String copied += extra default operator ? copyAppender.append(" Edition") stdout.println(base) stdout.println(copied)
Common mistakes
design — Capturing a mutable value directly does NOT isolate the closure from the caller. Mutating operators (+=, :=:, :~:) on a captured object affect the SAME object the caller sees. To isolate, copy the value via :=: or a copy constructor BEFORE capture.
Incorrect:
isolated <- (master) with trait of Appendable as class override append() -> extra as String master += extra //BUG: mutates the caller's master default operator ?
Correct:
snapshot <- String() snapshot :=: master isolatedAppender <- (snapshot) with trait of Appendable as class override append() -> extra as String snapshot += extra default operator ?
Other ways to ask this
- Are dynamic captures by value or by reference in EK9?
- If I capture a List into a closure and the closure mutates it, does the caller see the change?
- How do I take a snapshot of a value when capturing into a dynamic class?
- What is the EK9 equivalent of Java's effectively-final or JavaScript's closure-by-reference?
Coming from another language?
Java: lambda captures are effectively final — outer rebind impossible at the language level, but the captured object is shared by reference and mutating its state IS visible to the caller (e.g., captured List). Same issue, less explicit. JavaScript: closures capture by scope-chain reference for both rebind AND mutation — even more permissive than EK9. Rust: closures distinguish Fn (read-only borrow), FnMut (mutable borrow), FnOnce (move) at the type-system level — explicit ownership semantics make isolation vs sharing a compile-time property. Kotlin: lambdas capture by reference (val and var both); mutation of captured mutable objects is visible. EK9: capture is by-pointer; rebind (`:=`) inside closure is local-only, mutation operators are shared with caller. Use `:=:` or copy constructor for explicit snapshot.
Keywords: deep copy, shallow copy, capture, closure, copy constructor, isolation, dynamic, by reference, mutation, by value, :=:, snapshot