letrec-expressions
It is possible for a let-expression to bind an identifier to a
procedure:
> (let ((square (lambda (n) (* n n))))
(square 12))
144
Like any other binding that is introduced in a let-expression, this
binding is local. Within the body of the let-expression, it
supersedes any previous binding of the same identifier, but as soon as the
value of the let-expression has been computed, the local binding
evaporates.
However, it is not possible to bind an identifier to a recursively defined procedure in this way:
> (let ((count-down (lambda (n)
(if (zero? n)
'()
(cons (- n 1) (count-down (- n 1)))))))
(count-down 10))
reference to undefined identifier: count-down
The difficulty is that when the lambda-expression is
evaluated, the identifier count-down has not yet been bound,
so the value of the lambda-expression is a procedure that
includes an unbound identifier. Binding this procedure value to the
identifier count-down creates a new environment, but does not
affect the behavior of procedures that were constructed in the old
environment. So, when the body of the let-expression invokes
this procedure, we get the unbound-identifier error.
Changing let to let* wouldn't help in this case, since even
under let* the lambda-expression would be completely
evaluated before the binding is established. What we need is some variant
of let that binds the identifier to some kind of a place-holder and
adds the binding to the environment first, then computes the value
of the lambda-expression in the new environment, and then finally
substitutes that value for the place-holder. This will work in Scheme, so
long as the procedure is not actually invoked until we get into the body of
the expression. The keyword associated with this ``recursive binding''
variant of let is letrec:
> (letrec ((count-down (lambda (n)
(if (zero? n)
'()
(cons (- n 1) (count-down (- n 1)))))))
(count-down 10))
(9 8 7 6 5 4 3 2 1 0)
A letrec-expression constructs all of its place-holder bindings
simultaneously (in effect), then evaluates all of the lambda-expressions simultaneously, and finally replaces all of the
place-holders simultaneously. This makes it possible to include binding
specifications for mutually recursive procedures (which invoke each other)
in the same binding list:
> (letrec ((up-sum
(lambda (ls)
(if (null? ls)
0
(+ (down-sum (cdr ls)) (car ls)))))
(down-sum
(lambda (ls)
(if (null? ls)
0
(- (up-sum (cdr ls)) (car ls))))))
(up-sum (list 1 23 6 12 7)))
-21
;; which is 1 - 23 + 6 - 12 + 7.
Letrec-expressions can be used to separate the husk and the
kernel of a recursive procedure without having to define two procedures.
Here's an example repeated from the reading that introduced
tail-call elimination:
;;; index -- return the position of a given item in a ;;; given list ;;; Given: ;;; A, a value. ;;; LS, a list. ;;; Result: ;;; POSITION, an exact integer. ;;; Preconditions: ;;; None. ;;; Postcondition: ;;; If A is an element of LS, POSITION is the least ;;; position in LS that is occupied by A (using ;;; zero-based indexing); otherwise, POSITION is -1. (define index (lambda (a ls) (index-kernel a ls 0)))
;;; index-kernel -- return the position of a given item ;;; relative to a list from which a given number of elements ;;; have been stripped ;;; Given: ;;; SOUGHT, a value. ;;; LS, a list. ;;; BYPASSED, an exact integer. ;;; Result: ;;; POSITION, an exact integer. ;;; Preconditions: ;;; BYPASSED is non-negative. ;;; Postcondition: ;;; If SOUGHT is an element of LS, POSITION is the sum of ;;; BYPASSED and least position in LS that is occupied by ;;; SOUGHT (using zero-based indexing); otherwise, POSITION ;;; is -1. (define index-kernel (lambda (sought ls bypassed) (cond ((null? ls) -1) ((equal? (car ls) sought) bypassed) (else (index-kernel sought (cdr ls) (+ bypassed 1))))))
This works, but it's more stylish to construct the kernel procedure inside
a letrec expression, so that the extra identifier can be bound to it
locally:
;;; index -- return the position of a given item in a ;;; given list ;;; Given: ;;; A, a value. ;;; LS, a list. ;;; Result: ;;; POSITION, an exact integer. ;;; Preconditions: ;;; None. ;;; Postcondition: ;;; If A is an element of LS, POSITION is the least ;;; position in LS that is occupied by A (using ;;; zero-based indexing); otherwise, POSITION is -1. (define index (lambda (sought ls) (letrec ((kernel (lambda (rest bypassed) (cond ((null? rest) -1) ((equal? (car rest) sought) bypassed) (else (kernel (cdr rest) (+ bypassed 1))))))) (kernel ls 0))))
Notice, too, that since the recursive kernel procedure is now entirely
inside the body of the index procedure, it is not necessary to pass
the value of sought to the kernel as a parameter. Instead, the
kernel can treat sought as if it were a constant, since its value
doesn't change during any of the recursive calls.
The same approach can be used to perform precondition tests efficiently, by
placing them with the husk in the body of a letrec-expression and
omitting them from the kernel. For instance, here's how to introduce
precondition tests into the greatest-of-list procedure from the reading
on preconditions and postconditions:
;;; greatest-of-list: find the greatest element of ;;; a given list of real numbers ;;; Given: ;;; LS, a list of real numbers. ;;; Result: ;;; GREATEST, a real number. ;;; Precondition: ;;; LS is not empty. ;;; Postconditions: ;;; (1) GREATEST is an element of LS. ;;; (2) GREATEST is greater than or equal to ;;; every element of LS. (define greatest-of-list (lambda (ls) (letrec ((all-real? (lambda (ls) (or (null? ls) (and (real? (car ls)) (all-real? (cdr ls)))))) (kernel (lambda (rest) (if (singleton? rest) (car rest) (max (car rest) (kernel (cdr rest))))))) (if (or (not (list? ls)) (null? ls) (not (all-real? ls))) (error "greatest-of-list: The argument must be a non-empty list of reals.") (kernel ls)))))
Embedding the kernel inside the definition of greatest-of-list
rather than writing a separate greatest-of-list-kernel procedure has
another advantage: It is impossible for an incautious user to invoke the
kernel procedure directly, bypassing the precondition tests. The
only way to get at the recursive procedure to which kernel is
bound is to invoke the procedure within which the binding is established.
I've recycled the name kernel in this example to drive home the
point that local bindings in separate procedures don't interfere with one
another. Even if both procedures were active at the same time -- if, for
instance, one issued the call (index (greatest-of-list (list 5 3 7))
(list 18 6 14 7 2)) -- the correct kernel procedure would be
invoked in each case, because the correct local binding would supersede all
others.
let-expressionsletrec-expressions can be used in writing most of these
husk-and-kernel procedures. When there is only one recursive procedure to
bind, however, a Scheme programmer might well use yet another variation of
the let-expression -- the ``named let.''
The named let has the same syntax as a regular let-expression, except that there is an identifier between the keyword
let and the binding list. The named let binds this extra
identifier to a kernel procedure whose parameters are the same as the
variables in the binding list and whose body is the same as the body of the
let-expression. So, for example, one might write the index
procedure as follows:
;;; index -- return the position of a given item in a ;;; given list ;;; Given: ;;; A, a value. ;;; LS, a list. ;;; Result: ;;; POSITION, an exact integer. ;;; Preconditions: ;;; None. ;;; Postcondition: ;;; If A is an element of LS, POSITION is the least ;;; position in LS that is occupied by A (using ;;; zero-based indexing); otherwise, POSITION is -1. (define index (lambda (sought ls) (let kernel ((rest ls) (bypassed 0)) (cond ((null? rest) -1) ((equal? (car rest) sought) bypassed) (else (kernel (cdr rest) (+ bypassed 1)))))))
When we enter the named let, the identifier rest is bound to
the value of ls and the identifier bypassed is bound to 0,
just as if we were entering an ordinary let-expression. In
addition, however, the identifier kernel is bound to a procedure
that has rest and bypassed as parameters and the body of the
named let as its body. As we evaluate the cond-expression,
we may encounter a recursive call to the kernel procedure -- in
effect, we re-enter the body of the named let, with rest now
re-bound to the former value of (cdr rest) and bypassed to
the former value of (+ bypassed 1).
As another example, here's a tail-recursive version of sum that uses a named let:
;;; sum: find the sum of the numbers in a given list ;;; Given: ;;; LS, a list of exact numbers. ;;; Result: ;;; TOTAL, an exact number. ;;; Preconditions: ;;; None. ;;; Postconditions: ;;; TOTAL is the sum of all of the elements of LS, ;;; and is 0 if LS has no elements. (define sum (lambda (ls) (let kernel ((rest ls) (running-total 0)) (if (null? rest) running-total (kernel (cdr rest) (+ (car rest) running-total))))))