Local bindings

So far we've seen three ways in which a value can be associated with a name in Scheme:

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 procedure calls; their role in a let-expression simply to give names to certain values while the body of the expression is being evaluated. The outer parentheses in a binding list are ``structural,'' like the outer parentheses in a cond-clause -- they are there to group the pieces of the binding list together.

When Scheme encounters a let-expression, it begins by evaluating all of the expressions inside its binding specifications. 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.)


Exercise 1

What are the values of the following let-expressions?

  1. (let ((tone "fa")
          (call-me "al"))
      (string-append call-me tone "l" tone))
    
  2. ;; Solutions to 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))))
    
  3. (let ((total (+ 8 3 4 2 7)))
      (let ((mean (/ total 5)))
        (* mean mean)))
    

You may use DrScheme 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.


Exercise 2

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 ((total (+ 8 3 4 2 7))
      (mean (/ total 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 (/ total 5) before binding either of the names total and mean; since (/ total 5) can't be computed until total 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* ((total (+ 8 3 4 2 7))
       (mean (/ total 5)))
  (* mean mean))

The star in the keyword let* has nothing to do with multiplication. Just think of it as an oddly shaped letter.


Exercise 3

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.


Exercise 4

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
  (let ((square (lambda (n)
                  (* n n))))
    (lambda (first-leg second-leg)
      (sqrt (+ (square first-leg) (square second-leg))))))

Regardless of whether square is defined outside this definition, the local binding gives it the appropriate meaning within the lambda-expression that describes what hypotenuse-of-right-triangle does.


Exercise 5

Here is a procedure that takes a non-empty list of strings as argument and returns the longest string on the list (or one of the longest strings, if there is a tie).

(define longest-string-in-list
  (lambda (ls)
    (if (null? (cdr ls))
        (car ls)
        (longer-string (car ls) (longest-string-in-list (cdr ls))))))

This definition of the longest-string-in-list procedure includes a call to the longer-string procedure, which returns the longer of two given strings:

(define longer-string
  (lambda (left right)
    (if (<= (string-length right) (string-length left))
        left
        right)))

Revise the definition of longest-string-in-list so that the name longer-string is bound to the procedure that it denotes only locally, in a let-expression.

Note that there are two possible ways to do this: The definiens of longest-string-in-list can be a lambda-expression with a let-expression as its body, or it can be a let-expression with a lambda-expression as its body. Does the order of nesting affect what happens when the procedure is invoked? If so, which arrangement is better? Why?


This document is available on the World Wide Web as

http://www.cs.grinnell.edu/~stone/courses/scheme/local-bindings.xhtml

created February 26, 1997
last revised March 17, 2000

John David Stone (stone@cs.grinnell.edu)