Skip to content

Step 1 of 3 · Reading · ~3 min

Closures — Capturing the Environment

Closures & Classes

Closures — Functions That Capture State

A closure is a function bundled with the environment it was defined in. You already have almost everything you need — you just have to make sure LoxFunction remembers which environment to chain onto, rather than always starting from globals.

The environment chain

Every Environment has a reference to an enclosing environment, forming a chain up to the global scope. Variable lookup walks up this chain until it finds the name:

python

Capturing at definition time, not call time

The critical line is in visit_function_declaration: when you build the LoxFunction, pass in the environment active right now, at the point the fun statement executes — not the global environment, and not some environment computed later.

python

That captured environment is the closure. When the function is later called, LoxFunction.call creates a new environment for the parameters — but its enclosing is the closure, not whatever environment happens to be active at the call site:

python

This one line is the entire mechanism. It's what makes lexical (static) scoping work: a function always sees the variables that were in scope where it was written, regardless of where it's called from.

Walking through the counter example

fun counter() {
  var n = 0;
  fun inc() {
    n = n + 1;
    return n;
  }
  return inc;
}
var c = counter();
print c();  // 1
print c();  // 2
  1. Calling counter() creates a new environment E1 (enclosing = global) and defines n = 0 in it.
  2. The nested fun inc() { ... } declaration runs inside that call, so inc's closure is E1 — the environment holding n.
  3. counter returns the inc function object (a LoxFunction whose closure is E1) and the call to counter() ends — but E1 isn't garbage collected, because inc (assigned to c) still holds a reference to it.
  4. Each call c() creates a fresh call-environment enclosing E1, mutates n inside E1 via assign (which walks up the chain to find it), and returns the updated value.

Because n lives in E1, not in the transient per-call environment, it persists across calls to c() — that's the closure "remembering" state.

Edge cases to watch

  • Capturing by reference to the environment, not by copying the value: two closures created in the same scope share and mutate the same variable.
  • Closures created inside a loop each need their own environment per iteration if the loop body declares a fresh var each time — otherwise all closures end up sharing the same final value (a classic bug in many real languages, not just yours).
Up nextResolving & Binding VariablesClosures & Classes

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…