Fundamentals of Computer Science I: Media Computing (CS151.02 2007F)
[Skip to Body]
Primary:
[Front Door]
[Glance]

[Academic Honesty]
[Instructions]
Current:
[Outline]
[EBoard]
[Reading]
[Lab]
[Assignment]
Groupings:
[Assignments]
[EBoards]
[Examples]
[Exams]
[Handouts]
[Labs]
[Outlines]
[Projects]
[Readings]
[Reference]
Reference:
[Scheme Report (R5RS)]
[Scheme Reference]
[DrScheme Manual]
Related Courses:
[CSC151.01 2007F (Davis)]
[CSC151 2007S (Rebelsky)]
[CSCS151 2005S (Stone)]
This reading is also available in PDF.
Summary: We've learned a number of basic techniques for writing recursive functions, including the common 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.
Contents:
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 nonempty list of
spots.
(The list must be nonempty because there is no leftmost
element
of an empty list.)
> (spotlist.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 builtin
quotient
procedure requires that the second argument, the
divisor, be nonzero. 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. Once you learn
the sixP 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: ;;; spotlist.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 spots. ;;; 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 (null? (cdr
spots))
is #t
if spots
is a singleton,
#f
if color
has two or more elements.
Here, then, is the procedure definition:
(define spotlist.leftmost (lambda (spots) (if (null? (cdr spots)) (car spots) (spot.leftmost (car spots) (spotlist.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:
> (spotlist.leftmost null)
cdr: expects argument of type <pair>; given ()
If you think back to the tailrecursive version of difference
,
you may note another time that we had a special singleton case. When we
compute
n_{1} 
n_{2} 
n_{3} 
n_{k},
the base case is not we have nothing to subtract
, but rather
we have nothing to subtract from n_{1}
.
We didn't note the need for a singleton base case until we tried to write a tailrecursive version, but the need was there. Of course, that means that we might consider rewriting the nontailrecursive version, but that version gave us the wrong answer, anyway.
If you consider the examples you've studied over the past few days, you will see that there is a common form for most of the procedures. The form goes something like this
(define recursiveproc (lambda (val) (if (basecasetest?) (basecasecomputation val) (combine (partof val) (recursiveproc (simplify val))))))
For example, for the spotlist.leftmost
procedure,
spotlist.leftmost
.
spots
, our list of numbers.
(null? (cdr spots))
, which
checks whether spots
has only one element.
car
, which extracts
the one spot left in spots
.
car
, which extracts
the first spot in spots
.
cdr
, which drops the
first element, thereby giving us a simpler (well, smaller) list.
spot.leftmost
.
Similarly, consider the first complete version of sum
.
(define sum (lambda (numbers) (if (null? numbers) 0 (+ (car numbers) (sum (cdr numbers))))))
In the sum
procedure,
sum
.
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 numbers
.
cdr
, which drops the the
first element.
When you write your own recursive procedures, it's often useful
to start with the general structure and then to fill in the pieces.
When you are recursing over lists (as you have in our first explorations
of recursion), partof is almost always car
and
simplify is almost always codr
. There's a bit more
to the basecasetest, since we've used both (null? ___)
and (null? (cdr? ___))
. We may also find other techniques.
However, when you work with other kinds of information (as you will do soon), you'll have different techniques for extracting a piece of information, for simplifying the argument, and for deciding when you're done.
Note, also, that examples like filtering suggest a similar, but more complex structure for recursive procedures.
(define recursiveproc (lambda (val) (cond ((onebasecasetest?) (onebasecasecomputation val)) ((anotherbasecasetest?) (anotherbasecasecomputation val)) ... ((specialrecursivecasetest1?) (combine1 (partof1 val) (recursiveproc (simplify1 val)))) ((specialrecursivecasetest2?) (combine2 (partof2 val) (recursiveproc (simplify2 val)))) ... (else (combine (partof val) (recursiveproc (simplify val)))))))
However, in practice you will find that you rarely have more than two basecase tests (and mostly one) and rarely more than two recursive cases.
When we write tailrecursive procedures, we simply use the result of the recursive call, and don't combine it with anything. Here's a simple tail recursive pattern.
(define procedure (lambda (val) (procedurehelper initialresult val))) (define procedurehelper (lambda (sofar remaining) (if (basecasetest? remaining) (finalcomputation sofar) (procedurehelper (combine (partof remaining) sofar) (simplify remaining)))))
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
alldark?
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 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 dark, the car must clearly be dark, and in addition the cdr must be an alldark 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 alldark?
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.alldark? ;;; Parameters: ;;; colors, a list of rgb colors. ;;; Purpose: ;;; Determine whether all of the elements of a list of colors ;;; represent dark colors. ;;; Produces: ;;; alldark?, a Boolean. ;;; Preconditions: ;;; All the values in the list are rgb colors. ;;; Postconditions: ;;; alldark? is #t if all of the elements of values are dark. ;;; alldark? is #f if at least one element is not dark. (define rgb.alldark? (lambda (colors) (or (null? colors) (and (rgb.dark? (car colors)) (rgb.alldark? (cdr colors))))))
When colors
is the empty list, alldark?
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 alldark?
proceeds to evaluate the first test in the
and
expression. If the first element of colors
is
not dark, the test fails, so alldark?
stops, returning
#f
. However, if the first element of colors
is dark,
the test succeeds, so alldark?
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))))))
http://www.cs.grinnell.edu/~rebelsky/Courses/CS151/History/Readings/recursion.html
.
[Skip to Body]
Primary:
[Front Door]
[Glance]

[Academic Honesty]
[Instructions]
Current:
[Outline]
[EBoard]
[Reading]
[Lab]
[Assignment]
Groupings:
[Assignments]
[EBoards]
[Examples]
[Exams]
[Handouts]
[Labs]
[Outlines]
[Projects]
[Readings]
[Reference]
Reference:
[Scheme Report (R5RS)]
[Scheme Reference]
[DrScheme Manual]
Related Courses:
[CSC151.01 2007F (Davis)]
[CSC151 2007S (Rebelsky)]
[CSCS151 2005S (Stone)]
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 Mon Dec 3 09:54:19 2007.
The source to the document was last modified on Sun Oct 7 17:31:34 2007.
This document may be found at http://www.cs.grinnell.edu/~rebelsky/Courses/CS151/2007F/Readings/recursionrevisitedreading.html
.
You may wish to validate this document's HTML ; ;
Samuel A. Rebelsky, rebelsky@grinnell.eduhttp://creativecommons.org/licenses/bync/2.5/
or send a letter to Creative Commons, 543 Howard Street, 5th Floor,
San Francisco, California, 94105, USA.