Scheme’s variables have lexical scope, i.e., 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. E.g.,
(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
, i.e., 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. E.g.,
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. E.g.,
(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
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 let
s:
(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.
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 display
s 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
I.e., 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
We see that unlike add2
, the procedure bump‑counter
returns a different result each time it’s called, because it
side-effects something outside itself. One particularly useful
variant of this procedure generates a different random
number each time it’s called. Many Schemes provide this as a
primitive procedure called random
2:
When called with no argument, (random)
returns a
“pseudorandom” number in the interval [0, 1), i.e., between 0
(inclusive) and 1 (exclusive), such that the results are
uniformly distributed within that interval.
(random) => 0.6360226737551197 (random) => 0.8057127493871963 (random) => 0.8595213305159558
With only a little bit of arithmetic, random
can be used
to simulate events of known probability. Consider the throwing of
a fair, six-headed die: The outcomes 1, 2, 3, 4, 5, 6 are equally
likely. To simulate this, we divide the interval [0, 1) into six
equal segments, associate each segment with an outcome, and have
(random)
’s output decide which outcome occurred. One way to
do it is to multiply the random number n (0 ≤n < 1) by 6
and take the ceiling: I’ll leave you to convince yourself that
this takes on the value of each die face with 1/6 probability.
(define throw-one-die (lambda () (let ((result (ceiling (* (random) 6)))) result)))
throw‑two‑dice
simply calls throw‑one‑die
twice:
(define throw-two-dice (lambda () (+ (throw-one-die) (throw-one-die))))
It returns an integer between 2 and 12 inclusive — not all equally likely!
1 fluid‑let
is a nonstandard special
form. See section 8.3 for a definition
of fluid‑let
in Scheme.
2 Writing your own version
of random
in Scheme requires quite a bit of mathematical
chops to get something acceptable. We won’t get into that here.