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"