Naming: let
let is the only way to name an ordinary value:
(let name init body)
It evaluates init, binds the result to name, and evaluates body
in the extended scope. One binder per let — there is no binding
list, so multiple names take nested lets:
(let x 10
(let y 20
(+ x y)))
;=> 30
Inner bindings shadow outer ones:
(let x 1
(let x (+ x 1)
x))
;=> 2
Names are resolved before evaluation
The parser resolves every name to an environment position at parse time; the evaluator never sees a name, only an index. Two consequences are worth knowing about.
First, an unbound name is a parse error, reported before any part of the form runs:
(let x 1 (+ x y))
;! parse error: unbound variable: y [in list starting with Sym("+")] [in list starting with Sym("let")]
Second, since names are positions, scope is entirely static — a closure captures the environment it was built in, and no later binding can reach into it.
No top-level define
The floor has no define. A let scope closes with its body, and
when a top-level form finishes, its bindings are gone:
(let x 42 x)
;=> 42
x
;! parse error: unbound variable: x
Each top-level form is a closed program. This is not an oversight but
a division of labour: the floor is the machine the rest of the system
is built on, and the interactive conveniences — define, a prelude, a
persistent environment — are provided by the Purple session (Part IV),
in the language itself. When a floor program needs many definitions in
scope at once, it nests lets; lib/mk.naj binds an entire µKanren
library around a program that way, seventeen lets deep.