Functional Problem Solving (CSC 151 2014F) : Readings
Primary: [Front Door] [Schedule] - [Academic Honesty] [Disabilities] [Email] - [FAQ] [Teaching & Learning] [Grading] [Rubric] - [Calendar]
Current: [Assignment] [EBoard] [Lab] [Outline] [Reading]
Sections: [Assignments] [EBoards] [Examples] [Handouts] [Labs] [Outlines] [Readings]
Reference: [Setup] [VM] [Errors] - [Functions A-Z] [Functions By Topic] - [Racket] [Scheme Report (R5RS)] [R6RS] [TSPL4]
Related Courses: [Davis (2013F)] [Rebelsky (2014S)] [Weinman (2014F)]
Misc: [Submit Questions] - [SamR] [Glimmer Labs] [CS@Grinnell] [Grinnell] - [Issue Tracker (Course)]
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.
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.)
;;; Procedure: ;;; drawing-leftmost ;;; Parameters: ;;; drawing1, a drawing ;;; drawing2, a drawing ;;; Purpose: ;;; Determine the leftmost of drawing1 and drawing2. ;;; Produces: ;;; leftmost, a drawing. ;;; Preconditions: ;;; drawing1 and drawing2 have the correct form for drawings. ;;; Postconditions: ;;; leftmost is equal to either drawing1 or drawing2. ;;; leftmost either begins in the same column as both drawing1 and ;;; drawing2 or begins in the same column as one, and begins to the ;;; left of the other. (define drawing-leftmost (lambda (drawing1 drawing2) (if (< (drawing-left drawing1) (drawing-left drawing2)) drawing1 drawing2)))
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:
(define drawings-leftmost (lambda (drawings) (if (null? (cdr drawings)) (car drawings) (drawing-leftmost (car drawings) (drawings-leftmost (cdr drawings))))))
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 ()
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
v
1 -
v
2 -
v
3 -
v
k,
the base case is not “we have nothing to subtract”, but rather
“we have nothing to subtract from
v
1”.
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.
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 recursive-proc (lambda (val) (if (base-case-test? val) (base-case-computation val) (combine (partof val) (recursive-proc (simplify val))))))
For example, for the drawings-leftmost
procedure,
drawings-leftmost
.
drawings
, our
list of numbers.
(null? (cdr
drawings))
, which checks whether drawings
has only one element.
car
, which extracts
the one drawing left in drawings
.
car
, which extracts the first drawing in
drawings
.
cdr
, which drops the first element, thereby
giving us a simpler (well, smaller) list.
drawing-leftmost
.
Similarly, consider the first complete version of sum
.
;;; 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))))))
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 cdr
. There's a bit more to
the base-case-test, 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 recursive-proc (lambda (val) (cond [(one-base-case-test?) (one-base-case-computation val)] [(another-base-case-test?) (another-base-case-computation val)] ... [(special-recursive-case-test-1?) (combine-1 (partof-1 val) (recursive-proc (simplify-1 val)))] [(special-recursive-case-test-2?) (combine-2 (partof-2 val) (recursive-proc (simplify-2 val)))] ... [else (combine (partof val) (recursive-proc (simplify val)))])))
However, in practice you will find that you rarely have more than two base-case tests (mostly one) and rarely more than two recursive cases.
When we write tail-recursive 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) (procedure-helper initial-result initial-remaining))) (define procedure-helper (lambda (so-far remaining) (if (base-case-test? remaining) (final-computation so-far) (procedure-helper (combine (part-of remaining) so-far) (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 irgb-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:
irgb-all-dark?
should return
#t
when given the empty list.
and
to make sure that they are both satisfied.
Thus, irgb-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: ;;; irgb-all-dark? ;;; Parameters: ;;; colors, a list of integer-encoded 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 integer-encoded RGB colors. ;;; irgb-dark? is defined. ;;; 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 irgb-all-dark? (lambda (colors) (or (null? colors) (and (irgb-dark? (car colors)) (irgb-all-dark? (cdr colors))))))
When colors
is the empty list,
irgb-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 irgb-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 irgb-all-dark?
stops, returning #f
.
However, if the first element of colors
is dark,
the test succeeds, so irgb-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.
;;; Procedure: ;;; irgb-any-dark? ;;; Parameters: ;;; colors, a list of integer-encoded RGB colors. ;;; Purpose: ;;; Determine whether any of the elements of a list of colors ;;; represent dark colors. ;;; Produces: ;;; any-dark?, a Boolean. ;;; Preconditions: ;;; All the values in the list are rgb colors. ;;; irgb-dark? is defined. ;;; Postconditions: ;;; any-dark? is #t if at least one of the elements of values are dark. ;;; any-dark? is #f if all of the elements are not dark. (define irgb-any-dark? (lambda (colors) (and (not (null? colors)) (or (irgb-dark? (car colors)) (irgb-any-dark? (cdr colors))))))
And, once again, we can generalize.
(define any-____? (lambda (lst) (and (not (null? lst)) (or (____? (car lst)) (any-____? (cdr lst))))))
Recall the following procedure (from the reading on Recursion with Helper procedures) to find the brightest in a list of colors.
(define irgb-brightest (lambda (colors) (if (null? (cdr colors)) (car colors) (irgb-brighter (car colors) (irgb-brightest (cdr colors))))))
Using the common form for recursive procedures given above applied
to irgb-brightest
, fill in the blanks:
Recall the alternate procedure (from the reading on Recursion with Helper procedures) to find the brightest in a list of colors.
(define irgb-brightest (lambda (colors) (irgb-brightest-helper (car colors) (cdr colors)))) (define irgb-brightest-helper (lambda (brightest-so-far colors-remaining) (if (null? colors-remaining) brightest-so-far (irgb-brightest-helper (irgb-brighter brightest-so-far (car colors-remaining)) (cdr remaining-colors)))))
Using the common form for tail recursive procedures given above applied
to this version of irgb-brightest
, fill in the blanks: