When this matters
Use bindings to give a result a name, create a local scope, or deliberately keep state between calls. Most code needs only define and let; reach for set! only when shared mutation is part of the model.
See it run
LISPEX
(define x 1)
(define get (lambda () x))
(set! x 2)
(get)Observed result
OUTPUT
2Read the example
get closes over the global cell named x, not a frozen copy of the number 1. set! writes 2 into that same cell, so the later call observes 2. A new inner x would instead shadow the cell.
How to reason about it
- Lookup chooses the nearest lexical cell; shadowing does not mutate an outer binding.
letrecallocates cells before initializers and raises E321 when an uninitialized cell is read.- A duplicate top-level
defineupdates the existing global cell, preserving earlier closure references.
Choose quickly
| Form | Use it for | Visibility of initializers |
|---|---|---|
define | a top-level name or named procedure | current top-level environment |
let | independent local values | outer environment |
let* | locals that depend on earlier locals | each earlier binding |
letrec | mutually recursive local procedures | all allocated cells; early reads fail |
set! | updating an existing cell | no new binding is created |
A common mistake
set! on an unbound name is E303; it never creates a binding.
Current boundaries
- The internal uninitialized sentinel cannot be represented as a guest value.
Keep going
Procedures shows how closures capture these cells. The closures guide turns that rule into a small stateful example.