Fundamentals of Computer Science I (CS151 2003F)
[Skip to Body]
Primary:
[Front Door]
[Current]
[Glance]

[Honesty]
[Instructions]
[Links]
[Guidelines for Lab Writeups]
Groupings:
[EBoards]
[Examples]
[Exams]
[Handouts]
[Homework]
[Labs]
[Outlines]
[Readings]
[Reference]
Misc:
[Scheme Report]
[Glimmer Scheme Reference]
[CSC151.01 (Gum)]
[CSC151 2003S]
[CSC151 2002F]
[SamR]
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. In Scheme, the primary technique used for repetition is called recursion, and involves having procedures call themselves.
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 our find one root of the quadratic equation
as
(define root1 (lambda (a b c) (/ (+ ( 0 b) (sqrt ( (* b b) (* 4 a c)))) (* 2 a))))
Here, there are calls to addition, subtraction, division, multiplication, and
square root in the definition of root1
.
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 listref
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
listref
. 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
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 nonempty 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 nonempty list, we can use
(car numbers)
, which is the first element, and (cdr
numbers)
, which is the rest of the list.
So the problem is to find the sum of a nonempty 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 sum
procedure within its own definition to compute the sum of the elements of
the rest of a nonempty list. Add the first element to this sum, and we're
done!
;;; 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. ;;; total is a number. ;;; 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 stepbystep 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 repeatedly solving simpler problems strategy 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 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
null)
 a oneelement list.
To compute the sum of this oneelement list, we again invoke the
sum
procedure. A oneelement 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,
null
. 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
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 oneelement
list, you'll recall, is 83 plus the sum of null
, that is, 83 +
0, or just 83. The sum of the twoelement list is 12 plus the sum of the
(cons 83 null)
, that is, 12 + 83, or 95. Finally, the sum of
the original threeelement 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 threeelement 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 mouse pointer onto it and click the
left mouse button, and DrScheme will abandon its attempt to evaluate the
expression it's working on.
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 sum
procedure
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 write, you need to keep track of two different things:
Hence, we'll build our helper procedure with two parameters,
sumsofar
and 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 newsumhelper (lambda (sumsofar remaining) (if (test) basecase recursivecase)))
The recursive case is fairly easy. We add the first element of
remaining
to sumsofar
and continue
with the new sumsofar
and the rest of remaining.
To continue
, we simply call newsumhelper
again.
(define newsumhelper (lambda (sumsofar remaining) (if (test) basecase (newsumhelper (+ sumsofar (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 newsumhelper (lambda (sumsofar remaining) (if (null? remaining) basecase (newsumhelper (+ sumsofar (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 newsumhelper (lambda (sumsofar remaining) (if (null? remaining) sumsofar (newsumhelper (+ sumsofar (car remaining)) (cdr remaining)))))
Now we're ready to write the primary procedure whose responsibility it is
to call newsumhelper
. Like sum
,
newsum
will take a list as a parameter. That list will
become remaining
. What value should sumsofar
begin with? Since we have not yet added anything when we start, it
begins at 0.
(define newsum (lambda (numbers) (newsumhelper 0 numbers)))
Putting it all together, we get the following.
;;; Procedure: ;;; newsum ;;; 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. ;;; total is a number. ;;; If all the values in numbers are exact, total is exact. ;;; If any values in numbers are inexact, total is inexact. (define newsum (lambda (numbers) (newsumhelper 0 numbers))) ;;; Procedure: ;;; newsumhelper ;;; Parameters: ;;; sumsofar, a number. ;;; remaining, a list of numbers. ;;; Purpose: ;;; Add sumsofar 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. ;;; sumsofar must be a number. ;;; Postcondition: ;;; total is the result of adding together sumsofar and all of the ;;; elements of remaining. ;;; total is a number. ;;; If both sumsofar and all the values in remaining are exact, ;;; total is exact. ;;; If either sumsofar or any values in remaining are inexact, ;;; total is inexact. (define newsumhelper (lambda (sumsofar remaining) (if (null? remaining) sumsofar (newsumhelper (+ sumsofar (car remaining)) (cdr remaining)))))
Does this change make a difference in the way in which the sum is evaluated? Let's watch.
(newsum (cons 38 (cons 12 (cons 83 null)))) => (newsumhelper 0 (cons 38 (cons 12 (cons 83 null)))) => (newsumhelper (+ 0 38) (cons 12 (cons 83 null))) => (newsumhelper 38 (cons 12 (cons 83 null))) => (newsumhelper (+ 38 12) (cons 83 null)) => (newsumhelper 50 (cons 83 null)) => (newsumhelper (+ 50 83) null) => (newsumhelper 133 null) => 133
Note that the intermediate results for newsum
were different,
primarily because newsum
operates from left to right.
Often the computation for a nonempty 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, so that if, for
instance, we give it a list consisting of 13, 63, 1, 0, 4, and 78, it
returns a list consisting of 63, 0, and 4. We can use direct recursion to
develop such a procedure:
cons
to attach the car to the new list.
Translating this algorithm into Scheme yields the following definition:
(define filteroutnegatives (lambda (ls) (if (null? ls) null (if (negative? (car ls)) (filteroutnegatives (cdr ls)) (cons (car ls) (filteroutnegatives (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 nonempty list of real numbers.
> (greatestoflist (list 17 38 62/3 14/9 204/5 26 19))
204/5
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 builtin
quotient
procedure requires that the second argument, the
divisor, be nonzero. You should form the habit of noting and detailing
such preconditions as you write the initial comment for a procedure:
;;; Procedure: ;;; greatestoflist ;;; Parameters: ;;; numbers, a list of real numbers. ;;; Purpose: ;;; Find the greatest element of a given list of real numbers ;;; Produces: ;;; greatest, a real number. ;;; Preconditions: ;;; numbers is not empty. ;;; All the values in numbers are real numbers. That is, numbers ;;; contains only numbers, and none of those numbers are complex. ;;; Postconditions: ;;; greatest is an element of numbers (and, by implication, is real). ;;; greatest is greater than or equal to every element of numbers.
If a list of real numbers is a singleton, the answer is trivial  its only
element is its greatest element. 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 builtin procedure
max
to compare the car to the greatest element of the cdr,
returning whichever is greater.
We can test whether the given list is a singleton by checking whether its
cdr is an empty list. The value of the expression (null? (cdr
ls))
is #t
if ls
is a singleton,
#f
if ls
has two or more elements.
Here, then, is the procedure definition:
(define greatestoflist (lambda (numbers) (if (null? (cdr numbers)) (car numbers) (max (car numbers) (greatestoflist (cdr numbers))))))
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:
> (greatestoflist null)
cdr: expects argument of type <pair>; given ()
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 alleven?
that takes a given
list of integers and determines whether all of them are even. As usual, we
consider the cases of the empty list and nonempty lists separately:
vacuously truethat all of its elements are even  there is certainly no counterexample that one could use to refute the assertion. So
alleven?
should return #t
when given the empty
list.
all even, the car must clearly be even, and in addition the cdr must be an alleven list. We can use a recursive call to determine whether the cdr is alleven, and we can combine the expressions that test the car and cdr conditions with
and
to make sure that they are
both satisfied.
Thus alleven?
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:
;;; Procedure: ;;; alleven? ;;; Parameters: ;;; values, a list of integers. ;;; Purpose: ;;; Determine whether all of the elements of a list of integers ;;; are even. ;;; Produces: ;;; result, a Boolean. ;;; Preconditions: ;;; All the values in the list are integers. ;;; Postconditions: ;;; result is #t if all of the elements of values are even. ;;; result is #f if at least one element is not even. (define alleven? (lambda (values) (or (null? values) (and (even? (car values)) (alleven? (cdr values))))))
When values
is the empty list, alleven?
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 alleven?
proceeds to evaluate the first test in the
and
expression. If the first element of values
is
odd, the test fails, so alleven?
stops, returning
#f
. However, if the first element of values
is even,
the test succeeds, so alleven?
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.
http://www.cs.grinnell.edu/~rebelsky/Courses/CS151/History/Readings/recursion.html
.
[Skip to Body]
Primary:
[Front Door]
[Current]
[Glance]

[Honesty]
[Instructions]
[Links]
[Guidelines for Lab Writeups]
Groupings:
[EBoards]
[Examples]
[Exams]
[Handouts]
[Homework]
[Labs]
[Outlines]
[Readings]
[Reference]
Misc:
[Scheme Report]
[Glimmer Scheme Reference]
[CSC151.01 (Gum)]
[CSC151 2003S]
[CSC151 2002F]
[SamR]
Disclaimer:
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 Tue Dec 9 14:00:27 2003.
The source to the document was last modified on Sun Sep 21 21:21:39 2003.
This document may be found at http://www.cs.grinnell.edu/~rebelsky/Courses/CS151/2003F/Readings/recursion.html
.
; ; Check with Bobby
Samuel A. Rebelsky, rebelsky@grinnell.edu