let
You can create code blocks that have local variables using the let
special form.
You've seen local binding environments in other languages before. In C or Pascal you've probably seen blocks with local variables of their own, e.g., in C:
... { int x = 10; int y = 20; foo(x,y); } ...
Here we've got a block (inside curly braces) where local
variables named x
and y
are visible. (The same thing
can be done with begin
...end
blocks in Pascal.)
When we enter the block, storage is allocated for the local variables,
and the storage is initialized with the appropriate initial values. We
say that the variables are bound when we enter the block--the
names x
and y
refer to something, namely the storage
allocated for them. (In C, the storage for local variables may be
allocated on an activation stack.)
This is a simple but important idea--when you enter a scope, you "bind" a name to storage, creating an association (naming) between a name and a place you can put a value. (In later chapters, we'll see how interpreters and compilers keep track of the association between names and storage.)
Sometimes, we refer to the storage allocated for a variable as "its binding," but really that's a shorthand for "the storage named by the variable," or "the storage that the variable is bound to."
Inside the block, all references to the variables x
and y
refer
to these new local variable bindings. When execution reaches the end of the
block, these variable bindings cease to exist and references to x
or y
will again refer to whatever they did outside the block (perhaps
global variables, or block variables of some intermediate-level block, or
nothing at all).
In this example, all that happens inside the block is a call to the procedure
foo
, using the values of the block variables, i.e., 10
and
20
. In C or Pascal, these temporary variables might be allocated
by growing the stack when the block is entered, and shrinking it again
when the block is exited.
In Scheme, things are pretty similar. Blocks can be created
with let
expressions, like so:
... (let ((x 10) (y 20)) (foo x y)) ...
The first part of the let
is the variable binding
clause, which in this case two subclauses, (x 10)
and
(y 20)
. This
says that the let
will create a variable named x
whose initial value is 10
, and another variable y
whose initial value is 20
. A let
's variable binding
clause can contain any number of clauses, creating any number of
let
variables. Each subclause is very much like the name
and initial value parts of a define
form.
The rest of the let
is a sequence of expressions, called
the let body. The expressions are simply evaluated
in order, and the value of the last expression is returned
as the value of the whole let
expression. (The fact that
this value is returned is very handy, and will be important
in examples we use later.)
A let
may only bind one variable, but it still needs parentheses
around the whole variable binding clause, as well as around the
(one) subclause for a particular binding. For example:
... (let ((x 10)) (foo x)) ...
(Don't forget the "extra" parentheses around the one variable binding clause--they're not really extra, because they're what tells Scheme where the variable binding clause starts and stops. In this case, before and after the subclause that defines the one variable.)
In Scheme, you can use local variables pretty much the way you do in
most languages. When you enter a let
expression, the let
variables will be bound and initialized with values. When you exit the
let
expression, those bindings will disappear.
You can also use local variables differently, however, as we'll explain in later chapters. In general, the bindings for Scheme variables aren't allocated on an activation stack, but on the heap. This lets you keep bindings around after the procedure that creates them returns, which will turn out to be useful.
(You might think that this is inefficient, and it could be, but good Scheme compilers can almost always determine that it's not really necessary to put most variables on the heap, and avoid the cost of heap-allocating them. As with good compilers for most languages, most variables are actually in registers when it matters, so that the generated code is fast.)