Overview
Scoping defines where variables are visible and how names resolve to values during program execution.
Closures extend this by capturing the environment where a function was defined, enabling persistent state and first-class functions.
Note
Scope answers where a name is valid.
Binding answers to what that name refers.
Closures preserve both across time and function boundaries.
Lexical vs Dynamic Scope
Lexical (Static) Scope
Under lexical scope, a variable’s meaning depends on where it’s written in the source code — not where it’s called.
Example (ML-like pseudocode):
let x = 10
let f = (λy. x + y)
let x = 20
f 5 (* result: 15 *)x resolves to 10 because f was defined when x = 10.
Lexical scope uses the environment at definition time to resolve names. Most modern languages (ML, Python, JavaScript, Rust) follow this rule.
Dynamic Scope
Dynamic scope resolves names based on call order, using the runtime environment.
(setq x 10)
(defun f (y) (+ x y))
(let ((x 20)) (f 5)) ; ⇒ 25 (uses caller’s x)Warning
Dynamic scope can cause unpredictable behavior because variable references depend on the call stack rather than static structure.
Environments and Bindings
An environment (ρ) maps variable names to memory locations; a store (σ) maps locations to values.
ρ = { x ↦ ℓ₁, y ↦ ℓ₂ }
σ = { ℓ₁ ↦ 10, ℓ₂ ↦ 20 }
Evaluation looks up names through these structures:
lookup(x, ρ, σ) = σ(ρ(x))
Lexical scoping creates an environment chain — each new function introduces a new environment linked to its parent.
Example
Diagram (
lexical_scope_chain.svg)
Boxes for function scopes (
global,f,g).Arrows pointing to parent environments.
Show
fdefined inglobal, retaining link tox.
Binding: Creation and Rebinding
A binding associates a variable with a value (or a location).
-
Static binding: determined at compile-time.
-
Dynamic binding: can change at runtime via assignment.
Example:
let x = 5
let y = x + 1Here, x → 5 is a binding. If x is mutable (ref in ML), rebinding modifies the store, not the environment.
Shadowing
When a new variable hides a previous binding with the same name:
let x = 5 in
let x = 10 in x + 1 (* inner x shadows outer *)Tip
Shadowing is harmless if deliberate, but often hides bugs when used accidentally in nested scopes.
Example
Diagram (
shadowing_and_binding.svg)
Two nested environment boxes; the inner box bindsxagain, cutting off outer visibility.
Closures: Capturing the Environment
A closure is a pair ⟨function, environment⟩ — it remembers the bindings present when it was created.
Example:
let make_counter =
let r = ref 0 in
(λ(). r := !r + 1, λ(). !r)
let (inc, get) = make_counter()
inc(); inc(); get() (* ⇒ 2 *)Here, both inc and get share the same captured r.
The environment persists even after make_counter returns.
Mechanism
At creation time:
-
The function body and its defining environment form a closure.
-
When called later, evaluation uses the captured environment, not the caller’s.
Example
Diagram (
closure_capture_model.svg)
Closure box containing a pointer to environment
{ r ↦ ℓ₁ }.Both
incandgetreferencing the sameℓ₁.
Closures and Mutation
Closures can either:
-
Capture values (immutable model — e.g., purely functional languages), or
-
Capture locations (mutable model — e.g., ML, JavaScript).
In the latter, multiple closures share mutable state:
let make = λ(). let r = ref 0 in
(λ(). r := !r + 1, λ(). !r)
let (inc, get) = make()
inc(); inc(); get() (* ⇒ 2 *)Warning
Sharing mutable cells across closures can introduce aliasing bugs — e.g., updates in one closure affect others unexpectedly.
Partial Application and Closure Chains
Closures also emerge naturally through currying:
let mk = λx. λy. λz. x + y + z
let f = mk 1
let g = f 2
g 3 (* ⇒ 6 *)Each step creates a new closure capturing the previously bound variables:
mk 1 → closure with { x = 1 }
f 2 → closure with { x = 1, y = 2 }
This chain of captured environments enables higher-order abstractions without explicit state.
Common Pitfalls
Warning
Capturing loop variables incorrectly (e.g., late binding in Python closures).
Relying on dynamic variables that change unexpectedly.
Shadowing outer bindings unintentionally.
Mutating shared state between closures without isolation.
Tip
Prefer immutable captures or copying semantics when possible — especially in concurrent or asynchronous code.
Conceptual Summary
| Concept | Meaning | Example |
|---|---|---|
| Scope | Where names are visible | Lexical vs dynamic |
| Binding | Association name → value | x = 10 |
| Environment | Chain of scopes | {x ↦ ℓ₁} |
| Closure | Function + environment | λ(). r := !r + 1 |
| Shadowing | Local hides global | let x = ... in let x = ... |
Diagram Concepts
-
lexical_scope_chain.svg: static environment resolution. -
closure_capture_model.svg: closure capturing free variables. -
shadowing_and_binding.svg: variable hiding and rebinding.