The environment model replaces substitution when mutation exists. A closure is a procedure + the environment in which it was defined. Frames chain into scope.
Rules for evaluation
In the environment model, every expression is evaluated with respect to some environment. The two key rules:
To evaluate a combination, evaluate the subexpressions in the current environment, then apply the procedure to the arguments.
To apply a procedure, create a new frame that binds the parameters to the arguments, extend the procedure's environment with this frame, and evaluate the body in the new environment.
Scheme
; A procedure is created in the global environment
(define (square x)
(* x x))
; Calling (square 5):; 1. Create new frame E1 with x = 5; 2. E1 extends global env; 3. Evaluate (* x x) in E1; 4. x is found in E1 -> 5
(display (square 5)) (newline) ; 25
(display (square 12)) ; 144
Python
def square(x):
return x * x
# Each call creates a new local scope (frame)# x is bound in that scopeprint(square(5)) # 25print(square(12)) # 144
Applying simple procedures
When a procedure is applied, the new frame's enclosing environment is the one where the procedure was defined, not where it is called. This is lexical scoping.
Scheme
; Lexical scoping: the closure remembers where it was born
(define (make-adder n)
(lambda (x) (+ x n)))
(define add5 (make-adder 5))
(define add10 (make-adder 10))
; add5's closure points to a frame where n=5; add10's closure points to a different frame where n=10
(display (add5 3)) (newline) ; 8
(display (add10 3)) (newline) ; 13
(display (add5 (add10 1))) ; 16
Python
def make_adder(n):
returnlambda x: x + n
add5 = make_adder(5)
add10 = make_adder(10)
print(add5(3)) # 8print(add10(3)) # 13print(add5(add10(1))) # 16
Frames as repositories of local state
set! modifies a binding in place within its frame. The frame persists as long as the closure references it. This is how local state works: the frame is the state.
Scheme
; Each call to make-counter creates a fresh frame; The closure holds a reference to that frame
(define (make-counter)
(let ((count 0))
(lambda ()
(set! count (+ count 1))
count)))
(define c1 (make-counter))
(define c2 (make-counter))
; c1 and c2 have separate frames, separate state
(display (c1)) (newline) ; 1
(display (c1)) (newline) ; 2
(display (c1)) (newline) ; 3
(display (c2)) (newline) ; 1 โ independent
(display (c2)) ; 2
Internal defines create bindings in the procedure's frame. They are visible only within that procedure. This gives us block structure without any special mechanism.
Scheme
; Internal definitions are local to their frame
(define (sqrt x)
(define (good-enough? guess)
(< (abs (- (* guess guess) x)) 0.001))
(define (improve guess)
(/ (+ guess (/ x guess)) 2))
(define (sqrt-iter guess)
(if (good-enough? guess)
guess
(sqrt-iter (improve guess))))
(sqrt-iter 1.0))
; good-enough?, improve, sqrt-iter are not visible here
(display (sqrt 2)) (newline) ; ~1.4142
(display (sqrt 9)) (newline) ; ~3.0
(display (sqrt 100)) ; ~10.0
; Visualizing scope chains:; Each procedure call extends the environment
(define (f x)
(define (g y)
(define (h z)
(+ x y z)) ; h sees x from f, y from g, z from h
(h 30))
(g 20))
; Environment chain: h-frame -> g-frame -> f-frame -> global
(display (f 10)) ; 60
Python
def f(x):
def g(y):
def h(z):
return x + y + z # h sees all enclosing scopesreturn h(30)
return g(20)
print(f(10)) # 60
Notation reference
Concept
Scheme
Python
Frame
(define ...) in body
Local scope of a function call
Enclosing env
Where lambda was evaluated
Closure's __closure__
Binding
(define x 5)
x = 5
Mutation
(set! x 10)
x = 10 / nonlocal x
Block structure
Internal define
Nested def
Translation notes
Scheme's environment model maps directly to Python's LEGB rule (Local, Enclosing, Global, Built-in).
let in Scheme is syntactic sugar for an immediately-applied lambda, creating a new frame. Python has no exact equivalent; the closest is a nested function call.
Python's nonlocal keyword explicitly requests mutation of an enclosing frame's binding, making the set! operation visible in syntax.