skip to content

When you partially apply a function by capturing a mutable variable in a closure, what surprising behavior can occur, and how do you make the captured value stable?

level: seniorimportance: should knowfreq 22%

answer

  1. Closures capture the variable, not a snapshot
  2. Captured var shares a live Ref cell — sees later writes
  3. while + var i loop => all lambdas see final i
  4. Fix via function param or copy into a local val
  5. for/map bind a fresh val per element — safe to close over

basics

~20 s

Kotlin closures capture variables by reference, not by snapshot. If you capture a var and later change it, the partially applied function sees the new value. Capture a val (or copy the value into one) to lock it in.

solid answer

~50 s

Kotlin closures capture *variables*, not just their current values — and for a `var` the closure shares the live mutable cell (implemented via a `Ref` wrapper on the JVM). So a closure built for partial application that captures a `var` will observe later mutations, and a closure built inside a loop over the loop variable can all see the final value. To make a partially-applied argument stable, capture a `val`: either fix the value through a function parameter (`fun fix(x: Int): (Int) -> Int = { y -> x + y }` — `x` is effectively immutable per call) or copy into a local `val` before building the lambda (`val snapshot = current; { -> use(snapshot) }`). With `for ((i) ...)` and lambdas, prefer mapping/closing over the immutable loop element. The rule: capture immutables for predictable partial application; reach for a mutable capture only when you intentionally want shared, evolving state.

code

kotlin · 9 lines
kotlin
// Pitfall
var i = 0
val fns = mutableListOf<() -> Int>()
while (i < 3) { fns += { i }; i++ }
println(fns.map { it() })          // [3, 3, 3]

// Stable: fresh immutable per element
val safe = (0 until 3).map { n -> { n } }
println(safe.map { it() })         // [0, 1, 2]

go deeper

for a junior

Recognizes that changing a captured var afterward changes what the lambda sees, and knows to capture a val instead.

for a middle

Explains the loop capture pitfall and fixes it by closing over a fresh immutable binding.

for a senior

Describes the JVM Ref-boxing mechanism and prescribes function-parameter or local-val capture to stabilize partially applied arguments.

for a principal

Adds concurrency reasoning (shared captured var needs atomics/synchronization) and sets guidance to default to immutable capture in partial-application helpers.

## The trap: closures capture variables, not snapshots When a lambda captures a variable, Kotlin captures the **variable itself**, not a frozen copy of its value at capture time. For a `val` this is invisible (the value never changes). For a **`var`**, the closure shares the same mutable storage cell — on the JVM, Kotlin boxes the `var` into a `Ref.IntRef`/`ObjectRef` so both the outer code and the lambda read/write the same cell. ```kotlin var level = "INFO" val log: (String) -> Unit = { msg -> println("[$level] $msg") } log("starting") // [INFO] starting level = "WARN" log("careful") // [WARN] careful <-- the partial captured the variable, not "INFO" ``` If you *intended* to partially apply `log` with the level fixed to `INFO`, this is a bug: the fixed argument silently changed. ## The classic loop pitfall ```kotlin val makers = mutableListOf<() -> Int>() var i = 0 while (i < 3) { makers += { i } // all close over the SAME var i i++ } println(makers.map { it() }) // [3, 3, 3] — not [0, 1, 2] ``` Every lambda shares the one `i`, which ends at 3. ## How to make the captured value stable **1. Capture through a function parameter (a fresh immutable per call):** ```kotlin fun fixLevel(level: String): (String) -> Unit = { msg -> println("[$level] $msg") } val info = fixLevel("INFO") // INFO is locked in for this closure ``` A function parameter is effectively a fresh `val` for each invocation, so each returned closure captures its own stable value. **2. Copy into a local `val` before building the lambda:** ```kotlin var level = "INFO" val snapshot = level // immutable copy val log = { msg: String -> println("[$snapshot] $msg") } level = "WARN" // does not affect log ``` **3. Use a `val` loop element (Kotlin's `for` already binds a fresh val):** ```kotlin val makers = (0 until 3).map { n -> { n } } // n is a fresh val per element println(makers.map { it() }) // [0, 1, 2] ``` Kotlin's `for (x in xs)` and the `n` parameter of `map`'s lambda are each a **fresh immutable binding per iteration**, so closing over them is safe — unlike the manual `var i` while-loop above. ## The principle - Capture an **immutable** (`val`, a function parameter, or a fresh loop element) when partially applying, so the fixed argument can't change underneath you. - Capture a **mutable** (`var`) only deliberately, when you want the closure to track evolving shared state. This is also relevant under concurrency: a shared captured `var` mutated from multiple threads needs synchronization or atomics.

  • Why does for (x in xs) avoid the while-loop capture bug?
    Each iteration of for binds a fresh, effectively immutable x; closures capture distinct bindings, so each sees its own value. The manual while loop reuses one mutable var i shared by all closures.
  • How is a captured var represented on the JVM, and why does that explain the shared-mutation behavior?
    Kotlin boxes it into a Ref wrapper (e.g., IntRef/ObjectRef) so the outer scope and the lambda hold the same reference cell; writes through either are visible to both.

Capturing a var is like writing 'see whiteboard' on a sticky note — when someone erases the board, your note now points to whatever's there. Capturing a val is photographing the board.

saying these in an interview costs you the question

  • Believing a closure freezes the value at capture time
  • Not recognizing the while+var loop capture bug
  • Assuming capturing a var is always safe for partial application
  • Ignoring thread-safety of a shared captured var under concurrency

context