Lexical variables

Scheme’s variables have lexical scope, ie, they are visible only to forms within a certain contiguous stretch of program text. The global variables we have seen thus far are no exception: Their scope is all program text, which is certainly contiguous.

We have also seen some examples of local variables. These were the lambda parameters, which get bound each time the procedure is called, and whose scope is that procedure’s body. Eg,

(define x 9)
(define add2 (lambda (x) (+ x 2)))

x        =>  9

(add2 3) =>  5
(add2 x) =>  11

x        =>  9

Here, there is a global x, and there is also a local x, the latter introduced by procedure add2. The global x is always 9. The local x gets bound to 3 in the first call to add2 and to the value of the global x, ie, 9, in the second call to add2. When the procedure calls return, the global x continues to be 9.

The form set! modifies the lexical binding of a variable.

(set! x 20)

modifies the global binding of x from 9 to 20, because that is the binding of x that is visible to set!. If the set! was inside add2’s body, it would have modified the local x:

(define add2
  (lambda (x)
    (set! x (+ x 2))
    x))

The set! here adds 2 to the local variable x, and the procedure returns this new value of the local x. (In terms of effect, this procedure is indistinguishable from the previous add2.) We can call add2 on the global x, as before:

(add2 x) =>  22

(Remember global x is now 20, not 9!)

The set! inside add2 affects only the local variable used by add2. Although the local variable x got its binding from the global x, the latter is unaffected by the set! to the local x.

x =>  20

Note that we had all this discussion because we used the same identifier for a local variable and a global variable. In any text, an identifier named x refers to the lexically closest variable named x. This will shadow any outer or global x’s. Eg, in add2, the parameter x shadows the global x.

A procedure’s body can access and modify variables in its surrounding scope provided the procedure’s parameters don’t shadow them. This can give some interesting programs. Eg,

(define counter 0)

(define bump-counter
  (lambda ()
    (set! counter (+ counter 1))
    counter))

The procedure bump‑counter is a zero-argument procedure (also called a thunk). It introduces no local variables, and thus cannot shadow anything. Each time it is called, it modifies the global variable counter — it increments it by 1 — and returns its current value. Here are some successive calls to bump‑counter:

(bump-counter) =>  1
(bump-counter) =>  2
(bump-counter) =>  3

5.1  let and let*

Local variables can be introduced without explicitly creating a procedure. The special form let introduces a list of local variables for use within its body:

(let ((x 1)
      (y 2)
      (z 3))
  (list x y z))
=>  (1 2 3)

As with lambda, within the let-body, the local x (bound to 1) shadows the global x (which is bound to 20).

The local variable initializations — x to 1; y to 2; z to 3 — are not considered part of the let body. Therefore, a reference to x in the initialization will refer to the global, not the local x:

(let ((x 1)
      (y x))
  (+ x y))
=>  21

This is because x is bound to 1, and y is bound to the global x, which is 20.

Sometimes, it is convenient to have let’s list of lexical variables be introduced in sequence, so that the initialization of a later variable occurs in the lexical scope of earlier variables. The form let* does this:

(let* ((x 1)
       (y x))
  (+ x y))
=>  2

The x in y’s initialization refers to the x just above. The example is entirely equivalent to — and is in fact intended to be a convenient abbreviation for — the following program with nested lets:

(let ((x 1))
  (let ((y x))
    (+ x y)))
=>  2

The values bound to lexical variables can be procedures:

(let ((cons (lambda (x y) (+ x y))))
  (cons 1 2))
=>  3

Inside this let body, the lexical variable cons adds its arguments. Outside, cons continues to create dotted pairs.

5.2  fluid‑let

A lexical variable is visible throughout its scope, provided it isn’t shadowed. Sometimes, it is helpful to temporarily set a lexical variable to a certain value. For this, we use the form fluid‑let.1

(fluid-let ((counter 99))
  (display (bump-counter)) (newline)
  (display (bump-counter)) (newline)
  (display (bump-counter)) (newline))

This looks similar to a let, but instead of shadowing the global variable counter, it temporarily sets it to 99 before continuing with the fluid‑let body. Thus the displays in the body produce

100 
101 
102 

After the fluid‑let expression has evaluated, the global counter reverts to the value it had before the fluid‑let.

counter =>  3

Note that fluid‑let has an entirely different effect from let. fluid‑let does not introduce new lexical variables like let does. It modifies the bindings of existing lexical variables, and the modification ceases as soon as the fluid‑let does.

To drive home this point, consider the program

(let ((counter 99))
  (display (bump-counter)) (newline)
  (display (bump-counter)) (newline)
  (display (bump-counter)) (newline))

which substitutes let for fluid‑let in the previous example. The output is now

4
5
6

Ie, the global counter, which is initially 3, is updated by each call to bump‑counter. The new lexical variable counter, with its initialization of 99, has no impact on the calls to bump‑counter, because although the calls to bump‑counter are within the scope of this local counter, the body of bump‑counter isn’t. The latter continues to refer to the global counter, whose final value is 6.

counter =>  6


1 fluid‑let is a nonstandard special form. See sec 8.3 for a definition of fluid‑let in Scheme.