So far we've seen three ways in which a value can be associated with a name in Scheme:
The names of built-in procedures, such as cons and
quotient, are predefined. When Chez Scheme starts
up, these names are already bound to the procedures they denote.
The programmer can introduce a new binding by means of a definition. A definition may introduce a new equivalent for an old name, or it may give a name to a newly constructed value.
When a programmer-defined procedure is called, the parameters of the procedure are bound to the values of the corresponding arguments in the procedure call. Unlike the other two kinds of bindings, parameter bindings are local -- they apply only within the body of the procedure. Scheme discards these bindings when it leaves the procedure and returns to the point at which the procedure was called.
A let-expression in Scheme is an alternative way to create
local bindings. A let-expression contains a binding
list and a body. The body can be any expression, or sequence
of expressions, to be evaluated with the help of the local name
bindings. The binding list is a pair of structural parentheses enclosing
zero or more binding specifications; a binding specification, in
turn, is a pair of structural parentheses enclosing a name and an
expression. Here's an example of a binding list, taken from a
let-expression in a real Scheme program:
((next (car source)) (char-list '()))
This binding list contains two binding specifications -- one in which the
value of the expression (car source) is bound to the name
next, and the other in which the empty list is bound to the
name char-list. Notice that binding lists and binding
specifications are not themselves procedure calls; their role in a
let-expression simply to give names to certain values while
the body of the expression is being evaluated.
When Scheme encounters a let-expression, it begins by
evaluating the expressions inside all of its binding specifications and
collecting the results. Then the names in the binding specifications are
bound to those values. Next, the expressions making up the body of the
let-expression are evaluated, in order; the value of the last
expression in the body becomes the value of the entire
let-expression. Finally, the local bindings of the names are
cancelled. (Names that were unbound before the let-expression
become unbound again; names that had different bindings before the
let-expression resume those earlier bindings.)
What are the values of the following let-expressions?
(let ((tone "fa")
(call-me "al"))
(string-append call-me tone "l" tone))
;; solving the quadratic equation x^2 - 5x + 4
;;
(let ((discriminant (- (* -5 -5) (* 4 1 4))))
(list (/ (+ (- -5) (sqrt discriminant)) (* 2 1))
(/ (- (- -5) (sqrt discriminant)) (* 2 1))))
(let ((sum (+ 8 3 4 2 7)))
(let ((mean (/ sum 5)))
(* mean mean)))
You may use Chez Scheme to help you answer these questions, but be sure you can explain how it arrived at its answers.
Using a let-expression often simplifies an expression that
contains two or more occurrences of the same subexpression. The programmer
can compute the value of the subexpression just once, bind a name to it,
and then use that name whenever the value is needed again. Sometimes this
speeds things up by avoiding such redundancies as the recomputation of the
discriminant in the second example in exercise 1; in other cases, there is
little difference in speed, but the code may be a little clearer. For
instance, here is an alternative definition of the remove-all
procedure that was presented as Program 4.8 in the text (page 105):
(define remove-all
(lambda (item ls)
(if (null? ls)
'()
(let ((first-element (car ls))
(rest-of-result (remove-all item (cdr ls))))
(cond ((equal? first-element item) rest-of-result)
((pair? first-element)
(cons (remove-all item first-element) rest-of-result))
(else (cons first-element rest-of-result)))))))
One of the least attractive features of the text's version of this program
was the repetition of the recursive call (remove-all item (cdr
ls)) in three different places. Consolidating the repeated code and
giving a name to the value it returns makes it a little easier to
understand what the three cond-clauses are doing.
Rewrite the count-all-symbols procedure from the lab on deep recursion, using a
let-expression to consolidate repeated subexpressions in the
same manner.
As shown in the third example in exercise 1, above, it is possible to nest
one let-expression inside another. One might be tempted to
try to combine the binding lists for the nested
let-expressions, thus:
;; Combining the binding lists doesn't work!
;;
(let ((sum (+ 8 3 4 2 7))
(mean (/ sum 5)))
(* mean mean))
This wouldn't work (try it and see!), and it's important to understand why
not. The problem is that, within one binding list, all of the
expressions are evaluated before any of the names are bound.
Specifically, Scheme will try to evaluate both (+ 8 3 4 2 7)
and (/ sum 5) before binding either of the names
sum and mean; since (/ sum 5) can't
be computed until sum has a value, an error occurs. You have
to think of the local bindings coming into existence simultaneously rather
than one at a time.
Because one often needs sequential rather than simultaneous binding, Scheme
provides a variant of the let-expression that rearranges the
order of events: If one writes let* rather than
let, each binding specification in the binding list is
completely processed before the next one is taken up:
;; Using let* instead of let works!
;;
(let* ((sum (+ 8 3 4 2 7))
(mean (/ sum 5)))
(* mean mean))
The star in the keyword let* has nothing to do with
multiplication; just think of it as an oddly shaped letter.
Write a nested let-expression that binds a total of five
names, a, b, c, d,
and e, with a bound to 9387 and each subsequent
name bound to a value twice as large as the one before it --
b should be twice as large as a, c
twice as large as b, and so on. The body of the innermost
let-expression should compute the sum of the values of the
five names.
Write a let*-expression equivalent to the
let-expression in the previous exercise.
One can use a let- or let*-expression to create a
local name for a procedure:
(define hypotenuse-of-right-triangle
(lambda (first-leg second-leg)
(let ((square (lambda (n)
(* n n))))
(sqrt (+ (square first-leg) (square second-leg))))))
Regardless of whether square is defined outside this
procedure, the local binding gives it the appropriate meaning in the body
of the let-expression.
Rewrite the longest-on-list procedure from the first lab on recursion so that it incorporates
the longer-string procedure by means of a local binding.
This document is available on the World Wide Web as
http://www.math.grin.edu/~stone/courses/scheme/local-bindings.html
created February 26, 1997
last revised May 30, 1998