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