Go to the first, previous, next, last section, table of contents.

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.)


Go to the first, previous, next, last section, table of contents.