Names, Scope, and Changing Values

Give values names with define and let, then use set! to change a value that a procedure reads.

When this matters

Give values names with define and let. Use set! when a procedure needs to read an updated value on a later call. The link between a name and its value is called a binding, and each binding keeps its value in one cell.

See it run

Save this complete program as bindings.lspx. Install the Native CLI first, or run each example separately in the Playground.

LISPEX
(define x 1)
(define get (lambda () x))
(set! x 2)
(get)
SH
lispex run bindings.lspx

Output

OUTPUT
2

The command exits with status 0 and creates no files.

Read the example

get reads x when you call it. By then, set! has changed x from 1 to 2, so (get) returns 2.

A binding connects a name to a place that stores its value. That place is called a cell. set! updates the value in an existing cell.

A name's scope is the part of the program where it can be used. Lispex uses lexical scope, so the source structure determines which binding a name refers to.

Use a local name

An inner name can hide an outer name. Run this example separately from the first one.

LISPEX
(define x 1)
(define get (lambda () x))
(list (let ((x 10)) (set! x 11) x) (get))

Result

OUTPUT
(11 1)

The inner x has its own cell. Changing it to 11 leaves the outer x at 1. This is shadowing. get still reads the outer x, where the procedure was created.

Use an earlier local value

LISPEX
(let* ((x 6)
       (y (+ x 1)))
  (* x y))

Observed result

OUTPUT
42

let* creates names from top to bottom. The expression (+ x 1) can therefore use x, which is already 6. An expression that supplies a binding's starting value is called an initializer.

Let a local procedure call itself

LISPEX
(letrec ((f (lambda (n)
              (if (= n 0)
                  1
                  (* n (f (- n 1)))))))
  (f 5))

Observed result

OUTPUT
120

f computes the factorial of 5. letrec creates all its cells before evaluating the initializers. The procedure can therefore refer to f in its own body. Several procedures in one letrec can also call each other.

Reading a cell before it has a value raises E321. For example, (letrec ((x x)) x) reads x too early. Lispex keeps an internal marker in that cell until initialization. The marker is not a value a Lispex program can produce.

Redefine a top-level name

LISPEX
(define x 1)
(define get (lambda () x))
(define x 3)
(get)

Result

OUTPUT
3

A top-level definition sits outside a procedure or local binding form. Repeating define there updates the existing cell. The earlier get therefore reads the new value.

Choose quickly

FormUse it forVisibility of initializers
definea top-level name or named procedurecurrent top-level bindings
letindependent local valuesouter bindings
let*locals that depend on earlier localsouter bindings and earlier bindings in this let*
letreclocal procedures that call themselves or each otherall bindings in this letrec; reading an uninitialized cell raises E321
set!updating an existing cellthe bindings visible where set! appears

A common mistake

set! on an unbound name is E303. It never creates a binding.

Keep going

Read Procedures to define and call your own procedures. The closures guide shows when two counters share a value and when each keeps its own.

Procedures · Closures guide