List Recursion, Revisited
Summary:
We've learned a number of basic techniques for writing recursive
functions over lists, including the a pattern of recursion.
But these techniques aren't always the clearest or most elegant for
every case. Here, we extend your recursion toolbox to include a few
more techniques, particularly the identification of special base cases
and ways to write recursive predicates.
Singleton Base Cases
Sometimes the problem that we need an algorithm for doesn't apply to the
empty list, even in a vacuous or trivial way, and the base case for a
direct recursion instead involves singleton lists
-- that is, lists with only one element. For instance, suppose
that we want an algorithm that finds the leftmost element of a given
non-empty list of drawings. (The list must be non-empty because there
is no leftmost element
of an empty list.)
> (drawings-leftmost
(list (drawing-rectangle 10 0 20 20)
(drawing-rectangle 3 5 20 20)
(drawing-rectangle 6 2 20 20)
(drawing-rectangle 1 50 20 20)
(drawing-rectangle 8 5 20 20)))
(drawing rectangle 0 "" 1 50 20 20)
The assumption that the list is not empty is a
precondition for the meaningful
use of this procedure, just as a call to Scheme's built-in
quotient procedure requires that the second
argument, the divisor, be non-zero. A precondition is a requirement
that must be met in order for your procedure to work correctly.
You should form the habit of figuring out when such preconditions
are appropriate. With the Six-P technique for documenting procedures,
you should also make it a habit to document such preconditions as you
write the initial comment for a procedure:
;;; Procedure:
;;; drawings-leftmost
;;; Parameters:
;;; drawings, a list of drawings.
;;; Purpose:
;;; Find the leftmost drawing in drawings.
;;; Produces:
;;; leftmost, a drawing.
;;; Preconditions:
;;; drawings is not empty.
;;; All the values in drawings are drawings.
;;; Postconditions:
;;; leftmost is an element of drawings (and is, by implication, a drawing).
;;; For each drawing, d, in drawings, leftmost either starts in the same
;;; column as di, or to the left of d.
Alternately, we can make the structure of the list part of the
specification of the parameters (and therefore an implicit
precondition).
;;; Parameters:
;;; drawings, a nonempty list of drawings.
Whether you specify the precondition in the parameters or the
preconditions section is often a matter of personal taste. What
is most important is that you specify it somewhere.
Now that we've documented the procedure, let's think about how to
implement it.
If a list of drawings contains only one element, the answer is trivial --
its only
element is its leftmost. Otherwise, we can take the list apart
into its car and its cdr, invoke the procedure recursively to find the
leftmost of the cdr, and then try to figure out which comes first.
How do we figure whether or not one drawing is to the left of another?
We compare their left edge, which we can do with drawing-left. Let's use a helper procedure to compare the two drawings and
return the leftmost. (This is not
a recursive helper procedure. Rather, like rgb-darker,
it is a relatively straightforward procedure that simplifies our
recursive definitions.)
We can test whether the given list has a single element by
checking whether its cdr is an empty list. The value of the
expression (null? (cdr drawings)) is #t
if drawings has a single element and #f if
drawings has two or more elements. (It gives an error
if drawings has zero elements.)
Here, then, is the procedure definition:
If someone who uses this procedure happens to violate its precondition,
applying the procedure to the empty list, the Scheme interpreter notices
the error and prints out a diagnostic message:
> (drawings-leftmost null)
cdr: expects argument of type <pair>; given ()
Singleton Values and Difference
If you think back to the tail-recursive version of difference,
you may note another time that we had a special singleton case. When we
compute
v1 -
v2 -
v3 -
vk,
the base case is not we have nothing to subtract
, but rather
we have nothing to subtract from
v1
.
We didn't note the need for a singleton base case until we tried to write
a tail-recursive version, but the need was there. Of course, that means
that we might consider rewriting the non-tail-recursive version, but that
version gave us the wrong answer, anyway.
Using And and Or
Of course, these common forms are not the only way to define recursive
procedures. In particular, when we define a predicate that uses direct
recursion on a given list, the definition is usually a little simpler if
we use and- and or-expressions
rather than if-expressions. For instance,
consider a predicate rgb-all-dark? that takes a given
list of colors and determines whether all of them are dark. As usual,
we consider the cases of the empty list and non-empty lists separately:
Since the empty list has no elements, it is (as mathematicians say)
vacuously true
that all of its elements are dark --
there is certainly no counterexample that one could use to refute the
assertion. So rgb-all-dark? should return
#t when given the empty list.
For a non-empty list, we separate the car and the cdr. If the
list is to count as rgb-all dark
, the car must clearly
be dark, and in addition the cdr must be an all-dark list. We can
use a recursive call to determine whether the cdr is all dark, and
we can combine the expressions that test the car and cdr conditions
with and to make sure that they are both satisfied.
Thus, rgb-all-dark? should return #t
when the given list either is empty or has a dark first element and
all dark elements after that. This yields the following definition:
When colors is the empty list,
rgb-all-dark? applies the first test in the
or-expression, finds that it succeeds, and stops,
returning #t. In any other case, the first test
fails, so rgb-all-dark? proceeds to evaluate
the first test in the and-expression. If the first
element of colors is not dark, the test fails,
so rgb-all-dark? stops, returning #f.
However, if the first element of colors is dark,
the test succeeds, so rgb-all-dark? goes on to the
recursive procedure call, which checks whether all of the remaining
elements are dark, and returns the result of this recursive call,
however it turns out.
Here's a template for that solution.
(define all-____?
(lambda (lst)
(or (null? lst)
(and (____? (car lst))
(all-____? (cdr lst))))))
We can use a similar technique to ask if any
value in a list is dark. In this case, if there are no values in
the list, we know that no values are dark. Otherwise, we check if
the first value is dark. If it is, then some value must be dark.
The complicated part is getting the base case right (particularly
if we want to avoid using if). The standard technique
is to require that the list not be null (using not
and and). If the list is null,
(not (null? lst)) returns #f. And, since
(and #f ...) is #f, we get false back for
the empty list, just as we wanted.
And, once again, we can generalize.
(define any-____?
(lambda (lst)
(and (not (null? lst))
(or (____? (car lst))
(any-____? (cdr lst))))))