Closures That Remember Values

Build two counters and choose whether each keeps its own count or both update the same count.

Call one counter twice

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

LISPEX
(define counter (let ((n 0)) (lambda () (set! n (+ n 1)) n)))
(list (counter) (counter))
SH
lispex run counter.lspx

Output

OUTPUT
(1 2)

The command exits with status 0 and creates no files.

let gives n the starting value 0. lambda creates a procedure that adds 1 to n and returns the new value. counter names that procedure.

The first call changes n to 1; the second changes it to 2. The procedure can still use n after let has finished. A procedure that keeps access to names from its surrounding code this way is called a closure.

Give each counter its own count

Save the following program as separate-counters.lspx.

LISPEX
(define (make-counter)
  (let ((n 0))
    (lambda ()
      (set! n (+ n 1))
      n)))

(define counter-a (make-counter))
(define counter-b (make-counter))
(list (counter-a) (counter-b) (counter-a) (counter-b))
SH
lispex run separate-counters.lspx

Output

OUTPUT
(1 1 2 2)

The command exits with status 0 and creates no files.

Each call to make-counter runs let again and creates a separate n. Calling counter-a changes only its own count. counter-b starts at 0 and advances independently.

Let two counters share one count

Save the following program as shared-counters.lspx.

LISPEX
(define counters
  (let ((n 0))
    (list (lambda () (set! n (+ n 1)) n)
          (lambda () (set! n (+ n 1)) n))))

(define counter-a (car counters))
(define counter-b (car (cdr counters)))
(list (counter-a) (counter-b) (counter-a) (counter-b))
SH
lispex run shared-counters.lspx

Output

OUTPUT
(1 2 3 4)

The command exits with status 0 and creates no files.

Here let runs once. It creates one n and two procedures that both use it. car selects the first procedure; car after cdr selects the second.

Calls run from left to right. counter-a changes n to 1, then counter-b changes the same n to 2. The next calls produce 3 and 4.

What the closures keep

A binding connects a name to a storage location called a cell. Keeping access to that binding is called capture. A closure captures the cell, so it sees later changes to the value stored there.

ProgramWhere n is createdResult of alternating calls
Separate countersone new n per make-counter call(1 1 2 2)
Shared countersone n for both procedures in the same let(1 2 3 4)

In both programs, the returned procedures provide access to n after let finishes. Keep state local to the procedures that need it. When changing the code, check a sequence of calls such as the ones above.

If an inner binding hides an outer name, each has its own cell. set! changes the nearest binding determined by the source structure. Repeating a top-level define updates the original global cell, so earlier closures see the new value. The bindings manual shows both cases.

Check yourself

In separate-counters.lspx, replace (define counter-b (make-counter)) with (define counter-b counter-a). What will the final line return?

Answer

It returns (1 2 3 4). Both names now refer to the same procedure, which updates one n. Creating a second name for a procedure does not run make-counter again.

Keep going

Starting fresh, these programs produce the same counts when called in the same order under the same execution conditions. For a rule whose state can be supplied as input, continue with the decision-rule guide.

Bindings and scope · Decision records