Theory
A closure is a first-class function bundled together with the environment that was in scope when the function was defined. That frozen environment guarantees that every free variable inside the function body continues to point to the value it originally captured, no matter where or when the function is later called.
Closures are the key mechanism that enforces static (lexical) scoping. Under lexical scoping, each variable use is resolved to the nearest binding in the program text, producing referential transparency (the same expression always yields the same value). Without the preserved environment, calls would fall back on whatever bindings happen to be live at run timeโdynamic scopingโwhich breaks that guarantee.
Because the environment travels with the code, closures naturally support higher-order patterns such as partial application (let add1 = add 1) and functions that consume or produce other functions (doTwice inc). Each of these examples works because captured variables like x or f retain their original bindings inside every call.
Implementation
Below is a minimal structural recipeโillustrated in Haskell-like pseudocodeโfor turning an interpreter that already supports numbers, variables, and let into one that handles closures.
-- 1 โธ Extend the Value type
data Value
= VNum Int
| VClos Env Id Expr -- โจfrozenEnv , parameter , bodyโฉ
type Env = [(Id, Value)]
-- 2 โธ Building a closure (ฮป-abstraction)
eval env (Lam x body) =
VClos env x body -- freeze *current* env
-- 3 โธ Calling a function (application)
eval env (App e1 e2) =
case eval env e1 of
VClos frozen param body ->
let v2 = eval env e2
env' = (param, v2) : frozen -- extend the *frozen* env
in eval env' body
_ -> error "attempt to call a non-function"