Local bindings

Course links

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

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; in other cases, there is little difference in speed, but the code may be a little clearer. For instance, here is a procedure remove-all which removes all occurrences of a given item in a given list.

;;; remove-all: construct a copy of a given list, but lacking any
;;; occurrences of a given item

;;; Given:
;;;   ITEM, a value.
;;;   LS, a list.

;;; Result:
;;;   REVISED, a list.

;;; Preconditions:
;;;   None.

;;; Postcondition:
;;;   The elements of REVISED are exactly the elements of LS,
;;;   in the same relative order, except that ITEM is absent.

(define remove-all
  (lambda (item ls)
    (if (null? ls)
        null
        (cond ((equal? (car ls) item)
               (remove-all item (cdr ls)))
              ((pair? (car ls))
               (cons (remove-all item (car ls))
                     (remove-all item (cdr ls))))
              (else
               (cons (car ls) (remove-all item (cdr ls))))))))

One of the least attractive features of this definition is 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. While we're at it, we might as well do the same thing with the other repeated expression, (car ls). Here is the result:

;;; remove-all: construct a copy of a given list, but lacking any
;;; occurrences of a given item

;;; Given:
;;;   ITEM, a value.
;;;   LS, a list.

;;; Result:
;;;   REVISED, a list.

;;; Preconditions:
;;;   None.

;;; Postcondition:
;;;   The elements of REVISED are exactly the elements of LS,
;;;   in the same relative order, except that ITEM is absent.

(define remove-all
  (lambda (item ls)
    (if (null? ls)
        null
        (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)))))))

Here's a similar example, slightly more complicated: Consider the count-all-symbols procedure from the procedure from the lab on deep recursion. We can once again use a let-expression to consolidate repeated subexpressions in the same manner.

;;; count-all-symbols: determine how many of the
;;; ultimate constituents of a given list structure are
;;; symbols

;;; Given:
;;;   LS, a list.

;;; Result:
;;;   COUNT, a nonnegative integer.

;;; Precondition:
;;;   LS is a list.

;;; Postcondition:
;;;   COUNT is the number of symbols at all levels of LS.

(define count-all-symbols
  (lambda (ls)
    (if (null? ls)
        0
        (let ((symbols-in-cdr (count-all-symbols (cdr ls))))
          (cond ((list? (car ls))
                 (let ((symbols-in-car
                         (count-all-symbols (car ls))))
                   (+ symbols-in-car symbols-in-cdr))
                ((symbol? (car ls)) (+ 1 symbols-in-cdr))
                (else symbols-in-cdr)))))))

Nested bindings

It is possible to nest one let-expression inside another, thus:

(let ((sample-list '(a b c d e)))
  (let ((sample-cdr (cdr sample-list)))
    (length sample-cdr)))

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 ((sample-list '(a b c d e))
      (sample-cdr (cdr sample-list)))
  (length sample-cdr)))

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 '(a b c d e) and (cdr sample-list) before binding either of the names sample-list and sample-cdr; since (cdr sample-list) can't be computed until sample-list 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* ((sample-list '(a b c d e))
       (sample-cdr (cdr sample-list)))
  (length sample-cdr)))

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

Local names for procedures

One can use a let- or let*-expression to create a local name for a procedure:

;;; hypotenuse-of-right-triangle: compute the length of
;;; the hypotenuse of a right triangle, given the lengths
;;; of its legs

;;; Given:
;;;   FIRST-LEG and SECOND-LEG, both real numbers.

;;; Result:
;;;   HYPOTENUSE, a real number

;;; Preconditions:
;;;   FIRST-LEG and SECOND-LEG are strictly positive.

;;; Postcondition:
;;;   HYPOTENUSE is the length of the hypotenuse of a right
;;;   triangle with FIRST-LEG and SECOND-LEG as its legs.

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

I am indebted to Professor Ben Gum for his contributions to the development of this reading.