As we've already seen, it is commonplace for the body of a procedure to include calls to another procedure, or even to several others. Direct recursion is the special case of this construction in which the body of a procedure includes one or more calls to the very same procedure -- calls that deal with simpler or smaller arguments.
For instance, let's define a procedure called sum that takes one
argument, a list of numbers, and returns the result of adding all of the
elements of the list together. When it's finished, it will work like this:
> (sum '(3 5)) 8 > (sum '(91 85 96 82 89)) 443 > (sum '(-17/7 23/11 16/13)) 894/1001 > (sum '(-412)) -412 > (sum '()) 0
We can write up the specification in the form of a comment:
;;; 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.
Because the list to which we apply sum may have any number of
elements, we can't just pick out the numbers using list-ref and add
them up -- there's no way to know in general whether an element even exists
at the position specified by the second argument to list-ref. One
thing we do know about lists, however, is that every list is either (a)
empty, or (b) composed of a first element and a list of the rest of the
elements, which we can obtain with the car and cdr
procedures.
Moreover, we can use the predicate null? to distinguish between the
(a) and (b) cases, and conditional evaluation to make sure that only the
expression for the appropriate case is chosen. So our procedure definition
is going to look something like this:
(define sum
(lambda (ls)
(if (null? ls)
--- Compute the sum of the empty list here. ---
--- Compute the sum of a non-empty list here. --- )))
And we know that in computing the sum of a non-empty list, we can use (car ls), which is the first element, and (cdr ls), which is the
rest of the list.
The sum of the empty list is easy -- since there's nothing to add, the
total is 0. So the problem is to find the sum of a non-empty list, given
the first element and the rest of the list. Well, the rest of the list is
one of those ``simpler or smaller'' arguments that I mentioned above.
Since Scheme supports direct recursion, we can invoke the sum
procedure within its own definition to compute the sum of the elements of
the rest of a non-empty list. Add the first element to this sum, and we're
done!
;;; 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) (if (null? ls) 0 (+ (car ls) (sum (cdr ls))))))
At first, this may look strange or magical, like a circular definition: If
Scheme has to know the meaning of sum before it can process
the definition of sum, how does it ever get started?
The answer is what Scheme learns from a procedure definition is not so much the meaning of a word as the algorithm, the step-by-step method, for solving a problem. Sometimes, in order to solve a problem, you have to solve another, somewhat simpler problem of the same sort. There's no difficulty here as long as you can eventually reduce the problem to one that you can solve directly.
That's how Scheme proceeds when it deals with a call to a recursive
procedure -- say, (sum (cons 38 (cons 12 (cons 83 '())))). First,
it checks to find out whether the list it is given is empty. In this case,
it isn't. So we need to determine the result of adding together the value
of (car ls), which in this case is 38, and the sum of the elements
of (cdr ls) -- the rest of the given list.
The rest of the list at this point is the value of (cons 12 (cons 83
'())). How do we compute its sum? We call the sum procedure
again. This list of two elements isn't empty either, so again we wind up
in the alternate of the if-expression. This time we want to add 12,
the first element, to the sum of the rest of the list. By ``rest of the
list,'' this time, we mean the value of (cons 83 '()) -- a
one-element list.
To compute the sum of this one-element list, we again invoke the sum
procedure. A one-element list still isn't empty, so we head once
more into the alternate of the if-expression, adding the car, 83, to
the sum of the elements of the cdr, '(). The ``rest of the list''
this time around is empty, so when we invoke sum yet one more time,
to determine the sum of this empty list, the test in the if-expression succeeds and the consequent, rather than the alternate, is
selected. The sum of '() is 0.
We now have to work our way back out of all the procedure calls that have
been waiting for arguments to be computed. The sum of the one-element
list, you'll recall, is 83 plus the sum of '(), that is, 83 +
0, or just 83. The sum of the two-element list is 12 plus the sum of the
(cons 83 '()), that is, 12 + 83, or 95. Finally, the sum of
the original three-element list is 38 plus the sum of (cons 12 (cons
83 '())) that is, 38 + 95, or 133.
Here's a summary of the steps in the evaluation process.
(sum (cons 38 (cons 12 (cons 83 '()))))
--> (+ 38 (sum (cons 12 (cons 83 '())))))
--> (+ 38 (+ 12 (sum (cons 83 '()))))
--> (+ 38 (+ 12 (+ 83 (sum '()))))
--> (+ 38 (+ 12 (+ 83 0)))
--> (+ 38 (+ 12 83))
--> (+ 38 95)
--> 133
The process is exactly the same, by the way, regardless of whether we
construct the three-element list using cons, as in the example
above, or as (list 38 12 83) or '(38 12 83). Since we get
the same list in each case, sum takes it apart in exactly the same
way no matter what mechanism was used to build it.
The method of recursion works in this case because each time we invoke the
sum procedure, we give it a list that is a little shorter and so a
little easier to deal with, and eventually we reach the base
case of the recursion -- the empty list -- for which the answer can be
computed immediately.
If, instead, the problem became harder or more complicated on each
recursive invocation, or if it were impossible ever to reach the base case,
we'd have a runaway recursion -- a programming error that shows up
in DrScheme not as a diagnostic message printed in red, but as an endless
wait for a result. The designers of DrScheme's interface provided a Break button above the definition window so that you can interrupt a
runaway recursion: Move the pointer onto it and click the left mouse
button, and DrScheme will abandon its attempt to evaluate the expression
it's working on.
Often the computation for a non-empty list involves making another test. Suppose, for instance, that we want to define a procedure that takes a list of integers and ``filters out'' the negative ones:
> (filter-out-negatives (list -13 63 -1 0 4 -78)) (63 0 4)
We can use direct recursion to develop such a procedure:
If the given list is empty, there are no elements to filter out and also no elements to keep, so the correct result is the empty list.
If the given list is not empty, we examine its car and its cdr. We can use a call to the very procedure that we're defining to filter negative elements out of the cdr. That gives a list comprising all of its non-negative elements.
If the car of the given list -- that is, its first element -- is negative, we ignore the car and return the result of the recursive procedure call, without change.
Otherwise, we invoke cons to attach the car to the new list.
Translating this algorithm into Scheme yields the following definition:
;;; filter-out-negatives: construct a list comprising ;;; the non-negative elements of a given list ;;; Given: ;;; LS, a list of exact numbers. ;;; Result: ;;; NON-NEGATIVES, a list of exact numbers. ;;; Preconditions: ;;; None. ;;; Postconditions: ;;; The elements of NON-NEGATIVES are exactly ;;; the non-negative elements of LS. (define filter-out-negatives (lambda (ls) (if (null? ls) '() (if (negative? (car ls)) (filter-out-negatives (cdr ls)) (cons (car ls) (filter-out-negatives (cdr ls)))))))
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 greatest element of a given non-empty list of real numbers.
> (greatest-of-list '(1 2 3 4 5)) 5 > (greatest-of-list '(-17 38 62/3 -14/9 204/5 26 19)) 204/5 > (greatest-of-list '(-12)) -12
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. You should form the habit of noting and detailing such
preconditions as you write the initial comment for a procedure.
If someone who uses this procedure happens to violate its precondition, applying the procedure to the empty list, at this point we'll have to rely on DrScheme to notice the error and prints out a diagnostic message. In a <a href="preconditions-and-postconditions.xhtml">later reading</a>, we'll see how to take control of the process of diagnosing and reporting such errors.
Now let's consider how to define the greatest-of-list
procedure recursively. If a list of real numbers is a singleton, the
answer is trivial -- its only element is its greatest element. For the
base case, then, we can simply return the car of the singleton list.
Otherwise, we can take the list apart into its car and its cdr, invoke the
procedure recursively to find the greatest element of the cdr, and use
Scheme's built-in procedure max to compare the car to the
greatest element of the cdr, returning whichever is greater. Here's the
code:
;;; 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) (if (singleton? ls) (car ls) (max (car ls) (greatest-of-list (cdr ls))))))
This procedure definition can't stand alone, however, because of the call
to the predicate singleton?, which is not predefined in
Scheme. We'll have to develop and define it ourselves in order to make the
definition of greatest-of-list work.
As usual, we'll start by writing out sample calls:
> (singleton? '(alpha)) #t > (singleton? '(beta gamma) #f > (singleton? '(delta epsilon zeta eta theta iota kappa) #f > (singleton? '()) #f > (singleton? '((mu nu omicron))) #t
(The last example features a singleton list in which the only element is a
non-singleton list. The singleton? procedure inspects only
the top level.)
One approach to defining singleton? would be to compute the
length of the list and see whether the result of that computation is 1.
However, to determine the length of the list, the computer actually has to
traverse the list and tally every element. If the list is a long one, the
traversal involves a lot of unnecessary computation, since we know that
singleton? should return #f as soon as we see the
second element. A better approach is to check whether the given list,
while not itself empty, has a null cdr:
;;; singleton?: determines whether a given list has ;;; one and only one element ;;; Given: ;;; LS, a list. ;;; Result: ;;; OUTCOME, a Boolean. ;;; Preconditions: ;;; None. ;;; Postcondition: ;;; OUTCOME is #T if LS has one and only one element, ;;; #F if it has none or more than one. (define singleton? (lambda (ls) (and (not (null? ls)) (null? (cdr ls)))))
In writing the definition of greatest-of-list, we just took the
singleton? predicate and the max procedure for granted and
wrote the greatest-of-list algorithm in the simplest and most direct
way possible, not concerning ourselves until later with the fact that max is a built-in procedure and singleton? is not. This strategy
for developing programs has been called wish-list programming:
We ``wish'' that Scheme provided everything we ever need as a primitive,
and write each new procedure definition as if this wish were true; then we
go back and fill in the gap between fantasy and reality by writing
definitions for any called procedures that are not, in fact, primitive.
One advantage of this strategy is that procedures like singleton?
may also be useful in other programs that we'll create later on, and if
we're careful we can re-use the definition without change in those
programs. A Scheme programmer gradually builds up a library of useful
procedures. Think of such a library as a way of extending and customizing
Scheme -- a rather small and abstract language -- by adding one's own
near-primitive procedures.
Incidentally, here's what happens when the precondition on greatest-of-list is violated:
> (greatest-of-list '()) (bug) cdr: expects argument of type <pair>; given ()
Since '() is not a singleton, the alternative of the if-expression is evaluated. As it happens, the first subexpression that
DrScheme tries to evaluate is (cdr ls); since '() has no cdr,
DrScheme stops and reports the error.
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 all-even? that takes a given list of integers and
determines whether all of them are even:
> (all-even? (list 12 -116 0 738)) #t > (all-even? (list 88 332 -1)) #f > (all-even? (list 260 381 114) #f > (all-even? (list 474) #t > (all-even? '()) #t
In the last case, a logician would say that it is ``vacuously true'' that
all of the elements of the list are even -- since there are no elements,
there are no exceptions to the generalization, so it's true by default. To
put the same point another way: #t is the identity for the Boolean
operator and, just as 0 is the identity for addition; ``anding''
a Boolean value with #t leaves it unchanged, just like adding a
number to 0.
In defining the all-even? predicate, we consider the cases of
the empty list and non-empty lists separately, as usual:
As we just observed, all-even? 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 ``all even,'' the car must clearly be even, and in addition the
cdr must be an all-even list. We can use a recursive call to determine
whether the cdr is all-even, and we can combine the expressions that test
the car and cdr conditions with and to make sure that they are
both satisfied.
Thus all-even? should return #t when the given
list either is empty or has an even first element and all even elements
after that. This yields the following definition:
;;; all-even?: determine whether all of the elements ;;; of a given list of integers are even ;;; Given: ;;; LS, a list of integers. ;;; Result: ;;; OUTCOME, a Boolean. ;;; Preconditions: ;;; None. ;;; Postconditions: ;;; OUTCOME is #T if all of the elements of LS are even, ;;; #F if any of them is not even. (define all-even? (lambda (ls) (or (null? ls) (and (even? (car ls)) (all-even? (cdr ls))))))
When ls is the empty list, all-even? 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 all-even?
proceeds to evaluate the first test in the and-expression. If the
first element of ls is odd, the test fails, so all-even?
stops, returning #f. However, if the first element of ls is
even, the test succeeds, so all-even? goes on to the recursive
procedure call, which checks whether all of the remaining elements are
even, and returns the result of this recursive call, however it turns out.