all_lessons/SICP JavaScript/10 · The environment modellesson 10 / 22

The environment model of evaluation

Lesson 09 left us stranded: assignment gave us objects with persistent state, but it killed the substitution model — substitution has no notion of "the current value of a name" and no rule for changing it. This lesson installs the replacement. In the environment model, a name does not stand for a fixed value; it denotes a binding in a frame, and frames chain together into environments. Evaluation looks names up along that chain, and assignment changes a binding in place. Two rules do all the work, and — crucially — they explain exactly why each make_account call from Lesson 09 gets a private, persistent balance: a function object remembers the environment in which it was defined.

Book source
Maps to SICP §3.2 (the environment model of evaluation): frames, bindings, the enclosing-environment pointer, the two evaluation rules, and environment diagrams for make_account / block structure. We follow that arc but the diagrams, traces, and the closure-of-the-loop with Lesson 09 are original.
Linear position
Prerequisite: Lesson 09 (assignment + local state, and the proof that substitution is now undefined).
New capability: trace evaluation precisely with frames, bindings, enclosing environments, and function objects that remember their defining environment — and use that to explain closures and private persistent state exactly, not by analogy.
The plan
Four moves. (1) Define the machinery: bindings, frames, environments, and the function object as <code, defining-env>. (2) State the two rules — name lookup walks the frame chain; application makes a new frame whose parent is the function's defining environment. (3) Draw the full environment diagram for make_account and read off why each balance is private and persistent. (4) Use the model on block structure / internal declarations, and on assignment.

1 · The machinery — frames, environments, function objects

The substitution model had one kind of thing: an expression being rewritten. The environment model has three, and the move that makes state expressible is splitting "a name's value" into a name and a place that holds a value.

Binding
A pairing of a name with a value, e.g. balance: 100. Assignment changes the value side of an existing binding — this is the place that the substitution model lacked.
Frame
A table of bindings, plus a pointer to its enclosing (parent) frame. A frame is one level of scope.
Environment
A frame together with the whole chain of enclosing frames above it, ending at the global frame. A name's meaning is always relative to an environment.

The last piece is the one Lesson 09 secretly relied on. A function object is not just code. When you evaluate a function expression, you create a pair:

function object = ( the function's parameters & body , a pointer to the environment in which it was created )

That second component — the defining environment — is the entire secret of closures. The code is inert text; the environment pointer is what lets the function later reach the variables that were in scope where it was written.

2 · The two rules

Everything about evaluation reduces to two rules. Learn these and you can hand-execute any program with state.

Rule 1 — looking up a name
To find the value of a name in an environment, search the current frame for a binding of that name. If found, that is the value. If not, follow the enclosing-environment pointer to the parent frame and repeat. Continue up the chain to the global frame; if it is nowhere, the name is unbound. (The first binding found shadows any binding of the same name further up.)
Rule 2 — applying a function
To apply a function object to arguments: (1) create a new frame; (2) bind the function's parameters to the argument values in that frame; (3) set the new frame's enclosing pointer to the environment stored in the function object — its defining environment, not the environment of the caller; (4) evaluate the function body in this new environment.

Step (3) is the rule the whole model turns on, and the place every other operational model gets it wrong. The new frame's parent is where the function was defined, not where it was called. (Calling-environment parenting would be "dynamic scope," and closures would not work.) Assignment gets the obvious third rule: x = v finds the binding of x by Rule 1's search and changes its value in that frame; let x = v creates a new binding in the current frame.

3 · Worked example — the full environment diagram for make_account

Worked example — why each account's balance is private and persistent
Take the Lesson 09 account, run two openings, and trace the frames.
function make_account(balance) {
  function withdraw(amount) {
    if (amount <= balance) { balance = balance - amount; return balance; }
    else return "insufficient funds";
  }
  return withdraw;            // simplified: return the withdraw closure directly
}
const acc1 = make_account(100);
const acc2 = make_account(100);
acc1(50);   // 50
acc1(20);   // 30
acc2(50);   // 50  -- untouched by acc1
After the two make_account calls and acc1(50), the environment structure is:
            GLOBAL frame
            +-------------------------------------------------+
            | make_account: ---> [fn object: params(balance),  |
            |                      body...,  env = GLOBAL ]     |
            | acc1: ----------------+                           |
            | acc2: ------------+   |                           |
            +-------------------|---|---------------------------+
                   ^            |   |            ^
   enclosing-ptr   |            |   |            |  enclosing-ptr
                   |            v   v            |
        E1 (make_account call)   E2 (make_account call)
        +----------------------+ +----------------------+
        | balance: 100 -> 50   | | balance: 100         |   <-- two SEPARATE
        | withdraw: [fn,env=E1]| | withdraw: [fn,env=E2]|       balance bindings
        +----------------------+ +----------------------+
              ^                         ^
              |  acc1 points here       |  acc2 points here
        (acc1's defining env)     (acc2's defining env)
Read it off. Each call to make_account ran Rule 2: it built a new frame (E1, then E2) binding balance to 100, with parent GLOBAL. The withdraw closure created inside each call captured that frame as its defining environment — so acc1 is <withdraw-code, E1> and acc2 is <withdraw-code, E2>. When we call acc1(50), Rule 2 makes a fresh frame binding amount: 50 whose parent is E1 (acc1's defining env). Inside, balance is found by Rule 1 in E1 (value 100), and balance = balance - 50 rewrites E1's binding to 50. acc2's balance lives in E2 and is never touched.

Private: nothing reaches E1's balance except closures whose defining env is E1 — i.e. acc1. Persistent: E1 is not discarded when make_account returns, because acc1 still points into it; the binding survives between calls, which is exactly the "remembered state" of Lesson 09. The model explains both, with no hand-waving.

4 · The model on block structure and assignment

Internal declarations — functions and lets nested inside a function — are just frames within frames. When withdraw was declared inside make_account, evaluating that declaration in E1 created a binding withdraw: [fn, env=E1] in E1. So withdraw can see balance (it is in the same frame) and so can deposit (its parent is also E1) — they share state precisely because they share a defining frame. Block structure is not a separate feature; it falls straight out of Rule 2.

Question from Lesson 09Environment-model answer
What is the "current value" of a mutable name?The value in its binding now — found by Rule 1's chain search. There is now a "now": the contents of a frame at a point in time.
How does assignment change meaning?It overwrites the value side of an existing binding (located by Rule 1). No expression is rewritten — a place is updated.
Why does each closure keep private state?Each carries a pointer to its own defining frame (Rule 2, step 3); separate calls make separate frames.
Why does state persist after the function returns?The frame is kept alive by the closure that points into it; returning does not delete a frame that is still referenced.

Common mistakes / failure modes

Parenting the new frame to the caller
The single most common error. Rule 2 says the new frame's parent is the function's defining environment, not the caller's. Use the caller's and you get dynamic scope — closures break and private state leaks across calls.
Thinking a returned frame is gone
A frame survives as long as something references it. make_account returns, but E1 lives on because acc1 points into it — that is why the balance persists.
Confusing assignment with new binding
x = v searches up the chain and overwrites an existing binding; let x = v makes a fresh binding in the current frame, shadowing any above. Mixing them up changes which state you mutate.
One shared frame for "separate" objects
If two closures' defining environment is the same frame, they share state. Independent objects require independent frames — i.e. a fresh call to the maker each time.

Checkpoint exercise

Try it
Draw the environment diagram for this program and predict the three outputs:
function make_adder(n) {
  return function(x) { return x + n; };
}
const add10 = make_adder(10);
const add100 = make_adder(100);
add10(5);    // ?
add100(5);   // ?
add10(add100(5));  // ?
Answer. Each make_adder call builds a frame binding n (one with n:10, call it F10; one with n:100, F100). The returned closure captures that frame as its defining env: add10 = <fn, F10>, add100 = <fn, F100>. Calling add10(5) makes a frame x:5 with parent F10; Rule 1 finds n:1015. add100(5) → frame x:5, parent F100, n:100105. add10(add100(5)) = add10(105)115. The two ns never collide because they live in separate frames reached only through their own closure — same mechanism as the two balances.

Where this points next

The environment model restores an operational account of evaluation that handles state cleanly: a name denotes a binding in a frame, lookup walks the chain, and assignment overwrites a binding in place. So far, though, the mutable state has lived only in variables. Real systems put changing state inside data structures — a list whose cells are rewritten, a queue you push and pop, a table you update. Lesson 11 generalizes assignment from variables to compound data: set_head / set_tail mutate the pairs of Lesson 04 directly, and from that one capability we build mutable queues, tables, and an event-driven circuit simulator. The environment model still narrates it all — but the place being changed is now a field of a data object, not a frame binding.

Takeaway
The environment model replaces substitution with three things — bindings (name↦value places), frames (tables of bindings with a parent pointer), and function objects = (code, defining environment) — and two rules. Rule 1: look a name up by searching the current frame and walking the enclosing chain to global. Rule 2: apply a function by making a new frame that binds parameters to arguments and whose parent is the function's defining environment (not the caller's), then evaluate the body there. Assignment overwrites the value of an existing binding found by Rule 1. The make_account diagram reads off Lesson 09's mysteries directly: each call makes a separate frame (E1, E2) holding a separate balance; each withdraw closure captures its own frame; the frame survives because the closure points into it — hence private and persistent state. Block structure and internal declarations are just frames within frames. Next we move the mutable place from a frame binding into a data structure.

Interview prompts