CS 1321X - Lecture 9 - September 16, 2003

CS 1321X - Lecture 9

Everybody's Gone Recursin', Recursin' USA...


I.  All recursion, all the time

Given that there are some folks in class who still aren't comfortable with 
all aspects of recursion, let's take some time to look at where 
we started and how we got here, and maybe generalize some design principles 
for recursive solutions to simple problems.

We started with really really simple problems that didn't require repetition 
of any  kind.  We added numbers, subtracted numbers, and that sort of thing.  
And once we got past all that and understood the syntax of defining and 
calling functions, we moved into a world of repetitive operations that we 
could implement only through recursion, given the constraints of the purely 
functional programming paradigm that we were exploring.  So we started 
writing functions that took a number as an argument and returned some other 
number, and the recursion was controlled by some computation on that number.  
For example, the factorial function consumed a number and produced a number, 
to use the terminology used in the mainstream CS 1321 sections:

(define (factorial n)
  (cond [(= n 0) 1]
        [else (* n (factorial (- n 1)))]))

We could change the problem a little bit, and ask to add all the numbers from 
1 to n instead of multiplying all those numbers, and the solution is the same 
except for some minor adjustments:

(define (summation n)
  (cond [(= n 0) 0]
        [else (+ n (summation (- n 1)))]))

That's what we do when we recurse on a series of items, such as numbers, 
generated within the procedure.  We can extend this simple framework to 
recursing on a series of items generated not from within, but given to us 
in a data structure called a list:

(define (summation list-of-nums)
  (cond [(null? list-of-nums) 0]
        [else (+ (car list-of-nums)
                 (summations (cdr list-of-nums)))]))

It's the same basic framework.  But the termination condition is determined 
by the list data structure itself, not some specific value, so that changes.  
And because the series of things that you're dealing with comes in this data 
structure, you have to use your list-specific accessor functions, car and cdr,
to get at the things in the series.

Sometimes there are conditions on what you want to do.  For example, let's 
say you wanted to add up all non-negative numbers in a list for some weird 
reason:

(define (sum-non-neg list-of-nums)
  (cond [(null? list-of-nums) 0]
        [(>= (car list-of-nums) 0)
         (+ (car list-of-nums) (sum-non-neg (cdr list-of-nums)))]
        [else
         (sum-non-neg (cdr list-of-nums))]))

We reused the same basic structure...the only change here is that we put in 
an extra condition to test for when we wanted to add a given value to the 
total and when we wanted to skip it.  And there are a zillion different 
variations on this theme, but they'll all have this same look and feel...just 
maybe different tests and a different number of tests.  The fundamental 
mechanics of how it gets done are all going to be the same.

So then instead of consuming a list and producing some "atomic value", let's 
say we want to consume a list and produce some other list based on that list.  
It's something we do all the time.

Last week Bryan showed you the easiest sort of thing to do in this regard, 
and that was to make a copy of a list:

(define (make-copy inlist)
  (cond [(null? inlist) ()]
        [else (cons (car inlist) (make-copy (cdr inlist)))]))

This is just another tweak away from what we were doing before with a list of 
numbers, except we don't use arithmetic operators like + and *.  Since we're 
trying to construct a list, it stands to reason that we want to use our 
constructor function, cons, to make that list.  So we did.

But usually we don't want to make an exact copy.  We want that new list to be 
different in some way from the old list.  We add things to it, we move things 
around, we change the things in it, we delete things.  Let's say we have a 
list of numbers and we want to calculate the square roots of all those numbers:

(define (find-sqrts list-of-numbers)
  (cond [(null? list-of-numbers) ()]
        [else (cons (sqrt (car list-of-numbers))
                    (find-sqrts (cdr list-of-numbers)))]))

It's the same as make-copy, but we manipulated the elements of the list as we 
consed them together.  Slightly different problem, slightly different solution,
but the same old framework.

What if we wanted to do this again, but if the number is a negative number, 
we don't take the square root and just leave the original value in the list?

(define (find-sqrts-2 list-of-numbers)
  (cond [(null? list-of-numbers) ()]
        [(>= (car list-of-numbers) 0)
         (cons (sqrt (car list-of-numbers))
               (find-sqrts-2 (cdr list-of-numbers)))]
        [else (cons (car list-of-numbers)
                    (find-sqrts-2 (cdr list-of-numbers)))]))

It's the same thing as before, but we add another conditional clause to tell 
us what to do and when.  Now let's say we wanted to take a list of numbers 
and construct a list of only the square roots of the non-negative values and 
just make the negative values disappear.  We just change the action in the 
appropriate cond clause:

(define (find-non-neg-sqrts list-of-numbers)
  (cond [(null? list-of-numbers) ()]
        [(>= (car list-of-numbers) 0)
         (cons (sqrt (car list-of-numbers))
               (find-non-neg-sqrts (cdr list-of-numbers)))]
        [else
         (find-non-neg-sqrts (cdr list-of-numbers))]))

It's a different function, but still based on the same basic template.  If we 
want to delete all the negative values in the first place, before we even 
worry about square root of this or that, it's again the same basic function 
with just another tweak:

(define (delete-neg list-of-numbers)
  (cond [(null? list-of-numbers) ()]
        [(< (car list-of-numbers) 0)
         (delete-neg (cdr list-of-numbers))]
        [else
         (cons (car list-of-numbers) 
               (delete-neg (cdr list-of-numbers)))]))

And if you wanted to remove all the occurrences of some specific value from 
that list of numbers, we just add the parameter to pass that value to be 
deleted, and change a test accordingly:

(define (remove-item item inlist)
  (cond [(null? inlist) ()]
        [(equal? item (car inlist))
         (remove-item item (cdr inlist))]
        [else (cons (car inlist) 
                    (remove-item item (cdr inlist)))]))

At this point we could probably look back at all these examples and propose 
some standard patterns or templates that might help us in solving some of the 
problems that lie ahead:


;; recursion on a number (consume number, produce atomic value)

(define (name n)
  (cond [...some test on n... ...some base value... ]
        [else ...some operation on n...
              ...recurse on reduced n... ]))


;; recursion  on a list (consume list, produce atomic value)

(define (name inlist)
  (cond [...some test on inlist, usually(null? inlist)...
         ...some base value...]
        [else ...some op on (car inlist)...
              ...recurse on (cdr inlist)... ]))


;; some conditional handling maybe

(define (name inlist)
  (cond [...some test on inlist, usually(null? inlist)...
         ...some base value...]
        [...some test on (car inlist)...
         ...some op on (car inlist)...
         ...recurse on (cdr inlist)... ]
        [else ...some other op on (car inlist)...
              ...recurse on (cdr inlist)... ]))


;; recursion on a list (consume a list, produce a list)

(define (name inlist)
  (cond [...some test on inlist, usually(null? inlist)...
         ...some base value, usually ()...]
        [else ...(cons ...some op on (car inlist)...
              ...recurse on (cdr inlist)... ]))

and so on...

When you do the homework problems we give you, the idea is not just to see if 
you can do them by just pounding out the parentheses.  (Except for maybe
a couple of problems on the second assignment.  Sorry.)  The idea is to step 
back, analyze what's being asked for, and see if it looks like a pattern 
you're familiar with.  Lots of practice gets you more familiarity with the 
patterns that unfold.  The whole mainstream course is based on this idea of 
recognizing and using these patterns.

Now go and study for your test.  Do the practice problems posted on the
class web page.  Look at the exam from last year.  You get better with
practice, so go practice.  Now.



Copyright (c) 2003 by Kurt Eiselt.  All rights reserved, with 
the exception of stuff that belongs to somebody else.

Last revised: September 16, 2003