The Grinnell Scheme Web: Let-expressions

How do you create local bindings?

Use a let-expression. Here's how to put one together:

First, draw up a binding specification for each variable that you want to bind locally. Each binding specification consists of a left parenthesis, the variable to be bound, an expression, and a right parenthesis. The idea is that, as in a definition, the value of the expression will become the value of the variable -- it will be written on the three-by-five card that is inserted in the box representing the local environment. A typical binding expression looks like this:

(seconds-in-week (* 7 24 60 60))
Here seconds-in-week is the variable and (* 7 24 60 60) is the expression that provides its value.

Next, put a pair of parentheses around all of the binding specifications to form a binding specification list. Even if you're proposing to bind only one variable, you need this extra pair of parentheses so that Scheme will recognize that you want just one.

Now draw up the body of the let-expression, which is a sort of mini-program, a sequence of definitions and commands that are to be processed in the extended environment that includes the local bindings. The body may consist entirely of commands; if there are any definitions, they must come first, before any of the commands. There must be at least one command.

You can now assemble the entire let-expression, as follows: a left parenthesis, the keyword let, the binding specification list, the body, and a right parenthesis.

Here's a simple example, with only one local binding and a body comprising no definitions and one command:

(let ((number 5))
  (* number (+ number 1)))
This means: Let number be 5 and compute the product of number and number plus one. Five times six is thirty, so the value of this expression is 30:
> (let ((number 5))
    (* number (+ number 1)))
30
Can you run down the parts inventory on that expression one more time?

The outermost parentheses and the keyword let are the fixed syntax of the let-expression. ((number 5)) is the binding specification list; in this case, there's only one binding specification, (number 5). (* number (+ number 1)) is the body, in this case consisting just of the one command to compute the value of the expression. The command is a call to the * procedure with the arguments number and (+ number 1). This second argument is a call to the + procedure with the arguments number and 1.

OK, I guess. Now what's the difference between this and a top-level definition?

The binding that makes 5 the value of number can be examined only in the body of the let-expression. If the same variable occurs outside of the let-expression, a different environment, not containing that binding, is consulted instead. Watch what happens:

> (let ((number 5))
    (* number (+ number 1)))
30
> (+ 12 number)

ERROR: unbound variable:  number
; in expression: (... number)
; in top level environment.
The variable is unbound in the top-level environment, even though it is bound in the body of the let-expression. If you used a definition instead, you'd make an irreversible change in the top-level environment:
> (define number 5)
> (* number (+ number 1))
30
> (+ 12 number)
17
What happens if number is already bound before you introduce the local binding?

The local binding takes precedence inside the body of the let-expression. The previously established global binding holds both before and after:

> (define number 100)
> number
100
> (let ((number 5))
    (* number (+ number 1)))
30
> number
100
Replacing the let-expression in the preceding example to a global definition highlights the difference between a global and a local binding.
> (define number 100)
> number
100
> (define number 5)
> (* number (+ number 1))
30
> number
5
Let's see a more complicated example now. A slightly more complicated example.

OK. Suppose you want to calculate the cube of the sum of 181 and 263. One approach would be to write out a three-operand call to * and to compute the sum three times to get the three operands:

(* (+ 181 263) (+ 181 263) (+ 181 263))
But it would be better to compute it once, saving the result in a local variable, and then to use that variable as the repeated operand to *:
(let ((sum (+ 181 263)))
  (* sum sum sum))
Show me a let-expression that has more than one binding specification.

Here's one. How much larger is a rectangular solid that measures 12 units by 18 units by 24 units than one that is two units shorter in each dimension? In other words, compute the difference (in cubic units) between the volumes of the two solids.

(let ((length 12)
      (width 18)
      (height 24))
  (- (* length width height)
     (* (- length 2) (- width 2) (- height 2))))
Now show me one that has more than one command in its body.

Well, here's a trivial example. It's a variation on the ``Hello, world'' program:

(let ((sum (+ 5 7)))
  (display "I am delighted to report that the sum of 5 and 7 is ")
  (display sum)
  (display ".")
  (newline))
When executed in batch mode, this program generates the output line
I am delighted to report that the sum of 5 and 7 is 12.
In this case, the body of the let-expression consists of four separate commands -- three calls to the display procedure and one to the newline procedure.

Can you have one let-expression nested inside another -- say, with one being the body of the other?

Yes. The programmer can choose any commands she wants for the body of a let-expression, including a command to evaluate another let-expression. In fact, this comes in handy when you want to use the variable that is bound in the outer let-expression in the computation of the value of the variable bound in the inner let-expression:

> (let ((number 5))
    (let ((successor (+ number 1)))
      (* number successor)))
30
Couldn't you do the same thing just by putting both binding specifications in the same binding specification list?

No, that wouldn't work. Let me first show you what happens, and then explain why it happens:

> (let ((number 5)
        (successor (+ number 1)))
    (* number successor))
+: unbound variable: number
The problem is that the expressions that provide values for the variables in a binding specification list are all evaluated before any entries are added to the environment. When Scheme tries to evaluate (+ number 1), it needs a value for number, but the local binding for number doesn't exist yet.

In other words, the bindings specified in a single let-expression are created simultaneously, not one by one. The reason why the nested let-expression works is that the binding specified in the outer let-expression is in place before any part of the inner let-expression is evaluated.


Next topic
Previous topic
Table of contents


This document is available on the World Wide Web as

http://www.math.grin.edu/~stone/scheme-web/let.html


created June 8, 1995
last revised December 29, 1995

Copyright 1995 by John David Stone (stone@math.grin.edu)