100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

let and Local Bindings

Learn how let and let* create local variable bindings in LISP, and how their scoping rules differ from global assignment.

Functions & Control FlowIntermediate8 min readJul 10, 2026
Analogies

The let Special Form

let creates a new set of local variable bindings that exist only within its body: (let ((var1 val1) (var2 val2)) body). Critically, all the value expressions (val1, val2, ...) are evaluated using the bindings that existed before the let form — none of the new variables can see each other's values during initialization, since all bindings are established in parallel, not sequentially. This means (let ((x 1) (y x)) ...) will signal an error or use an outer x, not the x=1 just defined in the same let, because y's initializer is evaluated before x is bound.

🏏

Cricket analogy: let is like naming the entire playing XI on the team sheet simultaneously before the toss — every player's slot is decided from the pre-match squad list at once, so you can't reference 'the player just named at slot 3' to decide slot 4, since none of the new names exist yet during the naming process.

lisp
(let ((x 10)
      (y 20))
  (+ x y))          ; => 30

;; let* allows sequential access to earlier bindings
(let* ((x 10)
       (y (* x 2)))
  (+ x y))          ; => 30, y correctly sees x = 10

;; parallel let with the same names would fail to see each other:
;; (let ((x 10) (y x)) ...) would use the OUTER x, not 10

let* for Sequential Binding

When you need each binding to see the ones defined before it in the same form, use let* instead of let. let* evaluates and binds each variable one at a time, in order, so later initializers can reference earlier variables in the same let* — exactly the behavior that plain let disallows. This makes let* the natural choice whenever you're computing a chain of intermediate values, such as (let* ((width 10) (height (* width 2)) (area (* width height))) area), where height depends on width and area depends on both.

🏏

Cricket analogy: let* is like setting a batting order where each subsequent slot can be decided based on who's already been placed — you pick the opener first, then decide the number three based on who the opener is, sequentially building the lineup one slot at a time.

let and let* both create a new lexical scope: bindings established inside the form are invisible outside it, and they shadow any outer variable of the same name for the duration of the body, restoring the outer binding once the let form completes.

Lexical Scope and Shadowing

Common LISP variables are lexically scoped by default, meaning a variable reference resolves to the nearest enclosing binding in the source code, not based on the dynamic call chain at runtime. When a let binding uses the same name as an outer variable, it shadows the outer one for the extent of the let's body — references to that name inside the body see the new local value, and the outer binding becomes visible again once the let form's body finishes executing. This is distinct from special (dynamically scoped) variables, typically declared with defvar or defparameter and conventionally named with asterisks like *count*, which resolve based on the most recent dynamic binding on the call stack rather than lexical nesting.

🏏

Cricket analogy: Lexical shadowing is like a substitute fielder temporarily taking a position during an injury break — inside that specific passage of play, the substitute is 'the fielder at that position', but once the original player returns, the original identity resolves again, purely based on the current passage of play, not who's on the broader squad list.

Using let to 'rebind' a special variable (one declared with defvar/defparameter) does not create a lexical shadow the way it does for ordinary variables — it dynamically rebinds the variable for the duration of the let, affecting every function called within that body, not just code lexically inside it. This dynamic-vs-lexical distinction is a frequent source of confusion for programmers coming from lexically-scoped-only languages.

  • let creates local bindings whose initializer expressions are all evaluated using the prior (outer) environment, in parallel.
  • let* binds variables sequentially, allowing later initializers to reference earlier ones in the same form.
  • Both let and let* create a new lexical scope that ends when the form's body finishes executing.
  • A let binding shadows an outer variable of the same name only for the duration of the let's body.
  • Ordinary lexical variables resolve based on their position in the source code, not the runtime call chain.
  • Special variables (declared with defvar/defparameter, conventionally named with *earmuffs*) are dynamically scoped and rebinding them with let affects all code called within the body, not just lexically nested code.
  • Choose let* whenever a later binding's value depends on an earlier one defined in the same form.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#LISPStudyNotes#LetAndLocalBindings#Let#Local#Bindings#Special#StudyNotes#SkillVeris#ExamPrep