Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.