This reading is also available in PDF.
Summary: In many algorithms, you want to do things again and again and again. For example, you might want to do something with each value in a list. In general, the term used for doing things again and again is called repetition. While you have learned a few techniques for repeating code, these techniques only work with specific data structure. Hence, you should also know a more general technique, that will work with any instance in which you need to repeat code. In Scheme, the primary technique used for repetition is called recursion, and involves having procedures call themselves.
In your work so far, you've seen two basic types that ground repeated
code: You can do something for each position in an image, or you can
do something for each element of a list. You've also seen two general
strategies for what you do at each step: You can either update something
(such as an image), as is the case for
you can build a new compound value by applying a function to each value
in the original compound value, as in the case of
But what if you want to do other kinds of repetition? For example, what if you want to do something for every integer between 1 and 100, of if you want to do something for every third element of a list? What if you want to compute a list that contains only some elements of an original list? What if you want to compute a value that is based on all of the elements of an original list, such as the average of a list? Since it is not possible to predict every way that a programmer will want to repeat code, most languages provide a general mechanism for repeating instructions. In Scheme, the most general mechanism for repeating instructions is called recursion, and involves having a procedure call itself.
But what does it mean to have a procedure
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. For example, we might write instructions to find the average component as
(define average-component (lambda (color) (/ (+ (rgb.red color) (rgb.green color) (rgb.blue color)) 3)))
Here, there are calls to division, addition, and the three rgb component
procedures in the definition of
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:
> (sum (list 91 85 96 82 89)) 443 > (sum (list -17 17 12 -4)) 8 > (sum (list 19/3)) 19/3 > (sum null) 0
Because the list to which we apply
sum may have any number of
elements, we can't just pick out the numbers using
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
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
the structure of our definition is going to look something like this:
(define sum (lambda (numbers) (if (null? numbers) ; The sum of an empty list ; The sum of a non-empty list )))
The sum of the empty list is easy -- since there's nothing to add, the total is 0.
And we know that in computing the sum of a non-empty list, we can use
(car numbers), which is the first element, and
numbers), which is the rest of the list.
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 mentioned above.
Since Scheme supports direct recursion, we can invoke the
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! We'd express that portion as follows.
(+ (car numbers) (sum (cdr numbers)))
As we put it together into a complete procedure, we'll also add some documentation.
;;; Procedure: ;;; sum ;;; Parameters: ;;; numbers, a list of numbers. ;;; Purpose: ;;; Find the sum of the elements of a given list of numbers ;;; Produces: ;;; total, a number. ;;; Preconditions: ;;; All the elements of numbers must be numbers. ;;; Postcondition: ;;; total is the result of adding together all of the elements of numbers. ;;; If all the values in numbers are exact, total is exact. ;;; If any values in numbers are inexact, total is inexact. (define sum (lambda (numbers) (if (null? numbers) 0 (+ (car numbers) (sum (cdr numbers))))))
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.
Another way to think about it is in terms of the way we normally write
instructions. We often say
go back to the beginning and do the
steps again. Given that we've named the steps in the algorithm,
the recursive call is, in one sense, a way to tell the computer to go
back to the beginning.
The strategy of repeatedly solving simpler problems is how
Scheme proceeds when it deals with a call to a recursive
procedure -- say,
(sum (cons 38 (cons 12 (cons 83 null)))).
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
null)). How do we compute its sum? We call the
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
null) -- 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
adding the car, 83, to the sum of the elements of the cdr,
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
null 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
null, that is, 83 +
0, or just 83. The sum of the two-element list is 12 plus the sum of the
(cons 83 null), 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 null)) that is, 38 + 95, or 133.
Here's a summary of the steps in the evaluation process.
(sum (cons 38 (cons 12 (cons 83 null)))) => (+ 38 (sum (cons 12 (cons 83 null))))) => (+ 38 (+ 12 (sum (cons 83 null)))) => (+ 38 (+ 12 (+ 83 (sum null)))) => (+ 38 (+ 12 (+ 83 0))) => (+ 38 (+ 12 83)) => (+ 38 95) => 133
Talk about delayed gratification! That's a while to wait before we can do the first addition.
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
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. You'll need to restart DrFu to recover from infinite recursion. The good news is that you can hit thebutton to stop and save yoru code, so you can easily reload it after you restart.
As you may have noted, there are three basic parts to these kinds of recursive functions.
You'll come back to these three parts for each function you write.
As you may have noted, an odd thing about the
is that it works from right to left. Traditionally, we sum from left
to right. Can we rewrite
sum to work from left to right?
Certainly, but we may need a helper procedure (another procedure
whose primary purpose is to assist our current procedure) to do so.
If you think about it, when you're summing a list of numbers from left to right, you need to keep track of two different things:
Hence, we'll build our helper procedure with two parameters,
remaining. We'll start
the body with a template for recursive procedures (a test to
determine whether to use the base case or recursive case, the base
case, and the recursive case). We'll then fill in each part.
(define new-sum-helper (lambda (sum-so-far remaining) (if (test) base-case recursive-case)))
The recursive case is fairly easy. Recall that since
is a list, we can split it into two parts, the first element (that is, the
car), and the remaining elements (that is, the
Each part contributes to one of the parameters of the recursive
call. We update
sum-so-far by adding the first element of
sum-so-far. We update
remaining by removing the first element.
continue, we simply call
again with those updated parameters.
(define new-sum-helper (lambda (sum-so-far remaining) (if (test) base-case (new-sum-helper (+ sum-so-far (car remaining)) (cdr remaining)))))
The recursive case then gives us a clue as to what to use for the test. We need to stop when there are no elements left in the list.
(define new-sum-helper (lambda (sum-so-far remaining) (if (null? remaining) base-case (new-sum-helper (+ sum-so-far (car remaining)) (cdr remaining)))))
We're almost done. What should the base case be? In the previous version, it was 0. However, in this case, we've been keeping a running sum. When we run out of things to add, the value of the complete sum is the value of the running sum.
(define new-sum-helper (lambda (sum-so-far remaining) (if (null? remaining) sum-so-far (new-sum-helper (+ sum-so-far (car remaining)) (cdr remaining)))))
Now we're ready to write the primary procedure whose responsibility it is
new-sum will take a list as a parameter. That list will
remaining. What value should
begin with? Since we have not yet added anything when we start, it
begins at 0.
(define new-sum (lambda (numbers) (new-sum-helper 0 numbers)))
Putting it all together, we get the following.
;;; Procedure: ;;; new-sum ;;; Parameters: ;;; numbers, a list of numbers. ;;; Purpose: ;;; Find the sum of the elements of a given list of numbers ;;; Produces: ;;; total, a number. ;;; Preconditions: ;;; All the elements of numbers must be numbers. ;;; Postcondition: ;;; total is the result of adding together all of the elements of numbers. ;;; If all the values in numbers are exact, total is exact. ;;; If any values in numbers are inexact, total is inexact. (define new-sum (lambda (numbers) (new-sum-helper 0 numbers))) ;;; Procedure: ;;; new-sum-helper ;;; Parameters: ;;; sum-so-far, a number. ;;; remaining, a list of numbers. ;;; Purpose: ;;; Add sum-so-far to the sum of the elements of a given list of numbers ;;; Produces: ;;; total, a number. ;;; Preconditions: ;;; All the elements of remaining must be numbers. ;;; sum-so-far must be a number. ;;; Postcondition: ;;; total is the result of adding together sum-so-far and all of the ;;; elements of remaining. ;;; If both sum-so-far and all the values in remaining are exact, ;;; total is exact. ;;; If either sum-so-far or any values in remaining are inexact, ;;; total is inexact. (define new-sum-helper (lambda (sum-so-far remaining) (if (null? remaining) sum-so-far (new-sum-helper (+ sum-so-far (car remaining)) (cdr remaining)))))
Does this change make a difference in the way in which the sum is evaluated? Let's watch.
(new-sum (cons 38 (cons 12 (cons 83 null)))) => (new-sum-helper 0 (cons 38 (cons 12 (cons 83 null)))) => (new-sum-helper (+ 0 38) (cons 12 (cons 83 null))) => (new-sum-helper 38 (cons 12 (cons 83 null))) => (new-sum-helper (+ 38 12) (cons 83 null)) => (new-sum-helper 50 (cons 83 null)) => (new-sum-helper (+ 50 83) null) => (new-sum-helper 133 null) => 133
Note that the intermediate results for
new-sum were different,
new-sum operates from left to right.
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 colors and
filters out the dark ones. Let's assume that we've
already defined a predicate,
rgb.dark?, that determines whether
or not a color is dark. For most definitions of
if we started with the list of colors red, yellow, dark grey, green, blue, black, and white, we would end up with red, yellow, green, and white.
We can use direct recursion to develop such a procedure.
consto attach the car to the new list.
Translating this algorithm into Scheme yields the following definition:
(define rgb.filter-out-dark (lambda (colors) (if (null? colors) null (if (rgb.dark? (car colors)) (rgb.filter-out-dark (cdr colors)) (cons (car colors) (rgb.filter-out-dark (cdr colors)))))))
Of course, when you see nested
if expressions, you may
instead prefer to use a
cond. We can express the same idea
(define rgb.filter-out-dark (lambda (colors) (cond ((null? colors) null) ((rgb.dark? (car colors)) (rgb.filter-out-dark (cdr colors))) (else (cons (car colors) (rgb.filter-out-dark (cdr colors)))))))
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
(The list must be non-empty because there is no
element of an empty list.)
> (spots.leftmost (list (spot.new 10 0 color.white) (spot.new 3 5 color.white) (spot.new 6 2 color.white) (spot.new 1 50 color.white) (spot.new 8 5 color.white))) (1 50 -1)
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:
;;; Procedure: ;;; spot-list.leftmost ;;; Parameters: ;;; spots, a list of spots. ;;; Purpose: ;;; Find the leftmost spot in spots. ;;; Produces: ;;; leftmost, a spot. ;;; Preconditions: ;;; spots is not empty. ;;; All the values in spots are spots. That is, each is a list of ;;; three values, two integers and an RGB color. The first ;;; integer represents the column, the second the row, and the ;;; third the color. ;;; Postconditions: ;;; leftmost is an element of spots (and, by implication, is a spot). ;;; For each spot in spots, leftmost is either in the same column as ;;; the spot, or to the left.
If a list of spots is a singleton, 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 spot is to the left of another? We compare their columns. Let's use a helper procedure.
;;; Procedure: ;;; spot.leftmost ;;; Parameters: ;;; spot1, a spot ;;; spot2, a spot ;;; Purpose: ;;; Determine the leftmost of spot1 and spot2. ;;; Produces: ;;; leftmost, a spot. ;;; Preconditions: ;;; spot1 and spot2 have the correct form for sports. ;;; Postconditions: ;;; leftmost is equal to either spot1 or spot2. ;;; leftmost is either in the same column as both spot1 and spot2 or ;;; has the same column as one, and is to the left of the other. (define spot.leftmost (lambda (spot1 spot2) (if (< (spot.col spot1) (spot.col spot2)) spot1 spot2)))
We can test whether the given list is a singleton by checking whether its
cdr is an empty list. The value of the expression
spots is a singleton,
color has two or more elements.
Here, then, is the procedure definition:
(define spot-list.leftmost (lambda (spots) (if (null? (cdr spots)) (car spots) (spot.leftmost (car spots) (spot-list.leftmost (cdr spots))))))
If someone who uses this procedure happens to violate its precondition, applying the procedure to the empty list, DrScheme notices the error and prints out a diagnostic message:
> (spot-list.leftmost null) cdr: expects argument of type <pair>; given ()
If you consider the examples above, you will see that there is a common form for most of the procedures. The form goes something like this
(define recursive-proc (lambda (val) (if (base-case-test?) (base-case-computation val) (combine (partof val) (recursive-proc (simplify val))))))
For example, for the
spots, our list of numbers.
(null? (cdr spots)), which checks whether
spotshas only one element.
car, which extracts the one spot left in
car, which extracts the first spot in
cdr, which drops the first element, thereby giving us a simpler (well, smaller) list.
Similarly, consider the first complete version of
(define sum (lambda (numbers) (if (null? numbers) 0 (+ (car numbers) (sum (cdr numbers))))))
numbers, a list of numbers.
(null? numbers), which checks if we have no numbers.
0. (This computation does not quite match the form above, since we don't apply the 0 to
numbers. As this example suggests, sometimes the base case does not involve the parameter.)
car, which extracts the first value in
cdr, which drops the the first element.
Of course, this common form is 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
or-expressions rather than
instance, consider a predicate
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:
vacuously truethat all of its elements are even -- there is certainly no counterexample that one could use to refute the assertion. So
#twhen given the empty list.
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
andto make sure that they are both satisfied.
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:
;;; Procedure: ;;; rgb.all-dark? ;;; Parameters: ;;; colors, a list of rgb colors. ;;; Purpose: ;;; Determine whether all of the elements of a list of colors ;;; represent dark colors. ;;; Produces: ;;; all-dark?, a Boolean. ;;; Preconditions: ;;; All the values in the list are rgb colors. ;;; Postconditions: ;;; all-dark? is #t if all of the elements of values are dark. ;;; all-dark? is #f if at least one element is not dark. (define rgb.all-dark? (lambda (colors) (or (null? colors) (and (rgb.dark? (car colors)) (rgb.all-dark? (cdr colors))))))
colors is the empty list,
all-dark? applies the
first test in the
or-expression, finds that it succeeds, and
#t. In any other case, the first test fails,
all-dark? proceeds to evaluate the first test in the
and-expression. If the first element of
not dark, the test fails, so
all-dark? stops, returning
#f. However, if the first element of
colors is dark,
the test succeeds, so
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.
I usually create these pages
on the fly, which means that I rarely
proofread them and they may contain bad grammar and incorrect details.
It also means that I tend to update them regularly (see the history for
more details). Feel free to contact me with any suggestions for changes.
This document was generated by
Siteweaver on Mon Dec 3 09:54:17 2007.
The source to the document was last modified on Mon Sep 24 21:40:02 2007.
This document may be found at
You may wish to validate this document's HTML ; ;Samuel A. Rebelsky, email@example.com
http://creativecommons.org/licenses/by-nc/2.5/or send a letter to Creative Commons, 543 Howard Street, 5th Floor, San Francisco, California, 94105, USA.