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)
Write a letrec-expression in which (a) the identifier
last-of-list is locally bound to a procedure that finds and
returns the last element of a given list, and (b) the body of the
expression computes the sum of the last elements of the lists (3 8
2), (7), and (8 5 9 8), invoking
last-of-list three times.
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.
A non-empty list is an s-n-alternator if its elements are alternately symbols and numbers, beginning with a symbol. It is an n-s-alternator if its elements are alternately numbers and symbols, beginning with a number.
Write a letrec-expression in which (a) the identifiers
s-n-alternator? and n-s-alternator? are bound to
mutually recursive predicates, each of which determines whether a given
non-empty list has the indicated characteristic, and (b) the body invokes
each of these predicates to determine whether the list (2 a 3 b 4 c
5) fits either description.
The textbook consistently uses letrec-expressions to separate
the husk and the kernel of a recursive procedure without having to define
two procedures. Here's an example repeated from the lab that introduced tail-call
elimination:
;;; index: find the position of a given value in a given list.
;;; Givens:
;;; SOUGHT.
;;; LS, a list.
;;; Result:
;;; POSITION, an integer.
;;; Preconditions:
;;; None.
;;; Postconditions:
;;; (1) If SOUGHT is not an element of LS, then POSITION is -1.
;;; (2) If SOUGHT is an element of LS, POSITION is the least natural
;;; number k such that none of the first k elements of LS is SOUGHT,
;;; but the next one after the first k elements is SOUGHT.
(define index
(lambda (sought ls)
(index-kernel sought ls 0)))
(define index-kernel
(lambda (sought rest bypassed)
(cond ((null? rest) -1)
((equal? (car rest) sought) bypassed)
(else (index-kernel sought (cdr rest) (+ 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:
(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 lab on preconditions
and postconditions:
(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 (null? (cdr 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.
Define and test a version of the iota procedure that uses
letrec to pack a tail-recursive kernel inside a husk that
performs precondition testing. (Iota, you'll recall, takes a
natural number as argument and returns a list of all the lesser natural
numbers, in ascending order.)
The authors of our textbook use letrec-expressions in writing
most of these husk-and-kernel procedures. When there is only one recursive
procedure to bind, however, a contemporary 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:
(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:
(define sum
(lambda (ls)
(let kernel ((rest ls)
(running-total 0))
(if (null? rest)
running-total
(kernel (cdr rest) (+ (car rest) running-total))))))
Define and test a version of the iota procedure that uses
a named let.
Define and test a procedure named take that takes a list
ls and a non-negative integer len as arguments
and returns a list consisting of the first len elements of
ls, in their original order. The procedure should signal an
error if ls is not a list, if len is not an exact
integer, if len is negative, or if len is greater
than the length of ls.
Define and test a procedure named intersection that takes two
lists of symbols, left and right, as arguments
and returns a list of which the elements are precisely those symbols that
are elements of both left and right.
This document is available on the World Wide Web as
http://www.cs.grinnell.edu/~stone/courses/scheme/local-binding-and-recursion.xhtml
created March 2, 1997
last revised March 17, 2000