CS 2360 - January 21, 1997

Lecture 5 -- Recursion Wonderland


Last time we introduced the notion of recursion in LISP, and
this will prove to be very useful to you in the weeks to come
as it's the only control structure that will be available for
awhile.  (It's those functional programming constraints again...
you'll get used to it.)  We'll talk more about recursion today,
but first, let me mention something here that we didn't talk
about in class.


Something other than COND

There's another version of COND that's worth knowing 
about.  It's simpler than COND, but it's only useful in 
limited circumstances.  So long as there are no more than two 
alternatives, and so long as there's only one action 
associated with each alternative, IF works fine.  If your 
needs go outside these limitations, you'll want to start 
thinking about COND.

Here's the syntax for IF:

  (if *test*
      *then-action*
      *else-action*)

Here's the IF-based version of factorial:

  (defun factorial (n)
    (if (eql n 0)
        1
        (* n (factorial (- n 1)))))

And here's a rule of thumb to keep in mind:  when you feel 
like you need to nest your IFs, you probably would be better 
off with a COND.  And when you feel like you need to nest your
CONDs, you probably would be better off applying a heavy
dose of abstraction so that you don't create something 
really ugly.


Analyzing recursion

Now let's look at another example of recursion.  In this 
case, the example is a classic when it comes to educating 
folks about recursion.  It's computing the factorial of an 
integer.  The factorial function is defined in one of two 
ways:

n! = n * (n-1) * (n-2) * ... * 2 * 1 if n > 0
     1                               if n = 0

or

n! = n * (n-1)! if n > 0
     1          if n = 0

The first definition suggests a traditional iterative 
approach to computing the factorial, while the second one 
feels much more recursive.  So let's implement the second 
one:

  (defun factorial (n)
    (cond ((eql n 0) 1)
          (T (* n (factorial (- n 1))))))

If we apply our substitution model of evaluation, we can 
analyze what happens when we execute this program.  When we 
first invoke factorial, say on the integer 4, what goes on 
the program stack is the equivalent of this (we'll use "fact" 
instead of "factorial" to save space):

|        |        |        |        |        |        |
|        |        |        |        |        |        |
|        |        |        |        |        |        |
|        |        |        |        |        |        |
|        |        |        |        |        |        |
|        |        |        |        |        |        |
|        |        |        |        |        |        |
|        |        |        |        |        |        |
|        |        |        |        |        |        |
|(fact 4)|        |        |        |        |        |
-------------------------------------------------------

Evaluating (fact 4) results in replacing (fact 4) with the 
multiplication function and two arguments, the integer 4 and 
a function call of (fact 3):

|        |        |        |        |        |        |
|        |        |        |        |        |        |
|        |        |        |        |        |        |
|        |        |        |        |        |        |
|        |        |        |        |        |        |
|        |        |        |        |        |        |
|        |        |        |        |        |        |
|        |(fact 3)|        |        |        |        |
|        |   4    |        |        |        |        |
|(fact 4)|   *    |        |        |        |        |
-------------------------------------------------------

Again, what's on top of the stack gets evaluated and is 
subsequently replaced in our substitution model of 
evaluation:

|        |        |        |        |        |        |
|        |        |        |        |        |        |
|        |        |        |        |        |        |
|        |        |        |        |        |        |
|        |        |        |        |        |        |
|        |        |(fact 2)|        |        |        |
|        |        |   3    |        |        |        |
|        |(fact 3)|   *    |        |        |        |
|        |   4    |   4    |        |        |        |
|(fact 4)|   *    |   *    |        |        |        |
-------------------------------------------------------

This repeated substitution continues until we get to 
(fact 0), which is a termination condition evaluating to 1:

|        |        |        |        |        |        |
|        |        |        |        |(fact 0)|   1    |
|        |        |        |        |   1    |   1    |
|        |        |        |(fact 1)|   *    |   *    |
|        |        |        |   2    |   2    |   2    |
|        |        |(fact 2)|   *    |   *    |   *    |
|        |        |   3    |   3    |   3    |   3    |
|        |(fact 3)|   *    |   *    |   *    |   *    |
|        |   4    |   4    |   4    |   4    |   4    |
|(fact 4)|   *    |   *    |   *    |   *    |   *    |
-------------------------------------------------------

So we hit the termination condition of our recursion, and in 
the process of "unwinding" this recursion we return the 
values of all these stacked or postponed computations:

|        |        |        |        |        |        |
|   1    |        |        |        |        |        |
|   1    |        |        |        |        |        |
|   *    |   1    |        |        |        |        |
|   2    |   2    |        |        |        |        |
|   *    |   *    |   2    |        |        |        |
|   3    |   3    |   3    |        |        |        |
|   *    |   *    |   *    |   6    |        |        |
|   4    |   4    |   4    |   4    |        |        |
|   *    |   *    |   *    |   *    |   24   |        |
-------------------------------------------------------

Here's a slightly different notation for the substitution 
model of evaluation.  Maybe it'll give you a different 
perspective on what's happening.  It looks sort of like the 
results of using the TRACE function (which you'll learn about
in lab, if you haven't already):

(fact 4)
(* 4 (fact 3))
(* 4 (* 3 (fact 2)))
(* 4 (* 3 (* 2 (fact 1))))
(* 4 (* 3 (* 2 (* 1 (fact 0)))))
(* 4 (* 3 (* 2 (* 1 1))))
(* 4 (* 3 (* 2 1)))
(* 4 (* 3 2))
(* 4 6)
24

The interesting thing to note here is "shape" of the growth 
curve of the use of the program stack.  Each time the 
recursive call is made, we use up another chunk of program 
stack.  In fact, the use of memory grows linearly with the 
integer n in the call to factorial (i.e., memory use is O(n) 
for this algorithm).

What we see above is not just a picture of the factorial 
procedure (i.e., the code, the program, the algorithm), it's 
a look at the behavior of the "process"---the procedure in 
execution.  They are different, and it's important to be 
aware of the distinction.  In this case, we see a classic 
pattern of process behavior, and it even has a name.  Our 
recursive procedure gives rise to what's called a "linear 
recursive process".  

O(n) memory use is not exactly something to be proud of.  
We'd really like to do better than that.  "Uh?" you might be 
asking, "Is this the guy who told us not to worry about 
saving a cycle here or a byte there for the sake of 
efficiency?"  Yes, it's still me, but here we're looking at 
saving much more than a few cycles or a few bytes.  Here 
we're questioning whether we'll be able to compute factorials 
for large integers without running into limitations of 
available memory, and that's a whole different problem.  
Let's see if we can fix it.

Tail-recursion

If we go back and look at the shape of the factorial process 
above, we see that the real culprit in our excessive memory 
use is that each call to factorial gets replaced by a 
multiplication and another call to factorial.  We could stop 
this from happening if we could guarantee that each call to 
factorial is replaced only by another call to factorial (or 
something like it).  Here's how it's done:

(defun factorial (n)
  (factorial-iterative 1 1 n))

(defun factorial-iterative (product counter max-count)
  (cond ((> counter max-count) product)
        (T (factorial-iterative (* counter product)
                                (+ counter 1)
                                max-count))))

If we look at the behavior of the process that results from 
this procedure, we get an entirely different shape:

|        |               |               |               |
|(fact 4)|(fact-it 1 1 4)|(fact-it 1 2 4)|(fact-it 2 3 4)| (continued...)
----------------------------------------------------------

|               |                |    |
|(fact-it 6 4 4)|(fact-it 24 5 4)| 24 |
---------------------------------------

Or, using the other notation:

(fact 4)
(fact-it 1 1 4)
(fact-it 1 2 4)
(fact-it 2 3 4)
(fact-it 6 4 4)
(fact-it 24 5 4) 
24

Now, isn't that nice?  No growth in program stack space at 
all, and we didn't trade away an increase in execution time 
to get this either, as there are the pretty much the same 
number of function calls, multiplications, and so on.  So in 
this case, our memory usage is O(1) instead of O(n), and that 
is something to be proud of.  The shape is just a horizontal 
line, which is characteristic of what's called a "linear 
iterative process" (note that the process can be iterative, 
referring to how the shape is produced, even though the 
procedure itself is defined recursively).

To accomplish this, we used a type of recursion called "tail 
recursion".  In LISP, we obtain tail recursion by making sure 
that the step in our procedure which is the recursive step 
does not have any "work" being done "outside" the recursive 
function call.  Compare the corresponding steps from the two 
different function definitions.  Here's the first version:

          (T (* n (factorial (- n 1))))))

That "(* n " part is what was killing us in terms of memory 
usage.  This kind of recursion is called "augmenting 
recursion", meaning that there's some additional computation 
added on to the recursive call, and that makes the program 
stack grow.  In the tail recursive version, however, there's 
no augmenting recursion:  

          (T (factorial-iterative (* counter product)
                                  (+ counter 1)
                                  max-count))))

All the computation is done within the arguments to the 
function call, and so the substitutions are one-for-one and 
don't cause any growth of the program stack.

Here's a slightly different perspective which may help you
understand the difference between these two types of 
recursion.  The first version of the factorial function
postponed arithmetic operations as it progressed, and to
postpone those operations, LISP had to remember what those
operations were.  To remember all that stuff, LISP had to
use a chunk of memory on the program stack for each postponed
operation.

But when the tail-recursive version of factorial is running,
no arithmetic operations are being postponed.  All 
computations are being done as they're needed, so no 
operations must be remembered, and consequently there's
no corresponding growth in usage of the program stack.

In order to make this work, though, we had to introduce the 
equivalent of variables as place holders for these embedded 
computations, and we did this by introducing some additional 
arguments.  Those arguments in this case were established 
through the creation of a helping function, which initializes 
the arguments being used as variables:

(defun factorial (n)
  (factorial-iterative 1 1 n))

The arguments themselves take on the roles of the sorts of 
things you'd expect to see in a traditional iterative 
solution:

(defun factorial-iterative (product counter max-count)
  ...

Here, "product" is used as the place where you store your 
cumulative product, and it is initialized to the 
multiplicative identity (i.e., 1).  "counter" is just your 
index variable, and "max-count" is the limit on the number of 
"iterations" you want this thing to perform.

So, it may look like iteration, but tail recursion is most
definitely a clever and successful attempt to reap some
of the resource savings of iterative solutions while maintaining 
design qualities of the functional programming paradigm.
We gain the advantages of iterative solutions without the
corresponding increase in complexity.  What kind of complexity?
Well, just think about this for a minute.  If I'm debugging
a recursive function that's constructed within the constraints
of the functional programming paradigm (like "factorial-iterative"), 
I know that those "variables" won't be changing value while I 
examine the execution of that function.  That's gonna make my life 
easier, in the short run as well as the long run.  On the other 
hand, if I'm debugging the traditional, loop-based, side-effects-laden
equivalent of "factorial-iterative", I've got three variables
which *are* changing values as that procedure is running, and
that makes it harder for me to follow what's going on, and therefore
harder for me to make sure that the procedure actually does what I
want it to do.  And as we now know, that's the kind of problem that
can turn a relatively benign medical linear accelerator into a 
25MeV death ray in just a few keystrokes.  

Sometimes the tail recursive solutions are not immediately 
obvious, and sometimes they just seem natural.  Just take
 a look at "my-member" to see just how obviousthe tail recursive 
solution can be.  Like many things in this course, it's not 
especially hard, but it does require a different slant on 
thinking about the problem, and again like many things 
in this course, it gets better with practice.  In any case, going 
after the tail recursive solution is worthwhile, for reasons 
we've shown above.  Also, some LISP systems have the ability to 
recognize and further optimize tail recursive functions.  The 
object code that is produced is actually a simple loop (which 
you never see) with variables corresponding to the arguments being 
passed in the recursive function call.  This gives you the elegant 
expressiveness of recursion with the speed of loops or 
whatever your flavor of iterative control structure might be.  
You get all the good stuff, and none of the bad stuff.

You might argue that tail recursive solutions are harder to 
read, and I believe that's true for new LISP programmers.  
But as you gain more experience, the tail recursive solution 
becomes just another programming cliche and no longer causes 
any difficulty in understanding what's going on.  Remember that
lots of tasks look difficult the first time you encounter them,
but over time they become second nature.  When you were real 
little, your primary mode of locomotion was crawling.  At some
point you tried standing up and walking, and this was undoubtedly
much more difficult than crawling at first.  But you kept at it,
and now you get around by walking instead of crawling (except perhaps
the morning after one of those particularly intense frat parties).
The moral is simply that if you practice, you get better.


The myth of efficiency

One of the big myths that is constantly perpetuated in the 
religious wars about programming paradigms and programming 
languages is that functional programming in general (and LISP 
in particular) is to be avoided because of all this recursion 
stuff.  It's hard to learn, they say, and it's obviously 
inefficient.

It may be true that the concept of recursion itself is 
difficult to grasp, but for most folks it seems that the real 
problem is that they've been trained to think about 
repetitive computations in a certain way, using index 
variables and loops and so on.  That training interferes with 
thinking about the same problems in different ways; you'll 
feel that urge to fall back on old familiar habits.  That 
urge should diminish as you become more comfortable with the 
paradigm.  

But the knock on efficiency is in fact nothing more than 
mythology spread by folks who never learned the whole story.  
As we've just seen:

1)  If I'm thinking about it instead of acting like I'm brain
    dead, I can use tail recursion and get very efficient 
    results.  Remember that brain-dead programmers make 
    inefficient programs, regardless of their choice of 
    language or paradigm.

2)  Again, efficiency of the programmer, especially on the 
    back end of the software life cycle, is greatly improved
    by the use of recursion (and other functional programming
    ideas), assuming of course that the other folks who read
    your code also aren't brain dead.


Recursion templates

After exploring the factorial function in great detail, we 
spent the remainder of our time together in small groups, 
working out recursive solutions to small problems.  Each of 
these solutions served as a new example of a standard 
recursive form.  That is, while each solution used recursion, 
each was slightly different in either the way the test was 
done, what the recursive call looked like, or how the results 
were combined together.  We can think of each of these 
different forms as "recursion templates," and once you get 
the hang of how they're different and how they're similar, 
you'll probably find it easy to solve problems recursively by 
going to the appropriate template and plugging in the right 
stuff.  (The templates come from "Common LISP: A Gentle 
Introduction to Symbolic Computation" by David S. Touretzky.)


Augmenting recursion

The first example is called "augmenting recursion".  This 
refers to the fact that in the COND clause which contains the 
recursive call, there's work being done outside the recursive 
call itself.  This work augments the recursion, and in our 
substitution model of evaluation, that's what makes the 
program stack grow each time the recursive call is made.  
That in turn gives us that linear (or worse) recursive 
process shape that we've looked at before.  We saw
augmenting recursion when we constructed our first factorial 
function.  Here's another example of augmenting recursion.  
It's our own version of LISP's "length" function, which 
returns the number of top-level elements of a list:

(defun my-length (my-list)
  (cond ((null my-list) 0)
        (T (+ 1 (my-length (rest my-list))))))


Single-test tail recursion

As you'll recall, tail recursion is that magic solution which 
allows us to use the elegance and readability of a 
recursively-defined procedure while gaining none of the nasty 
memory usage associated with augmenting recursion.  
Furthermore, some LISP systems recognize tail recursion and 
optimize the object code to run as a simple loop, so we get 
better speed and bounded memory usage.  Neat, huh?  In this 
example, we constructed the tail recursive version of the my-
length function, and we used a helping function to set it all 
up:

(defun my-length (my-list)
  (my-length-tr my-list 0))

(defun my-length-tr (my-list counter)
  (cond ((null my-list) counter)
        (T (my-length-tr (rest my-list) (+ 1 counter)))))


Multiple-test tail recursion

In LISP, the function "nthcdr" takes two arguments, an 
integer and a list.  The "nthcdr" function then counts down 
the number of list elements indicated by the integer (by 
taking successive "rest"s or "cdr"s) and returns the list 
without those first elements.  For example:

? (nthcdr 0 '(a b c))
(A B C)
? (nthcdr 2 '(a b c))
(C)
?

Here's our own version of "nthcdr":

(defun my-nthcdr (integer my-list)
  (cond ((null my-list) nil)        
        ((eql integer 0) my-list)
        (T (my-nthcdr (- integer 1) (rest my-list)))))

There are at least three interesting things to note here.  
First, we used tail recursion again.  In fact, it just seemed 
like the obvious thing to do.  Second, we use more than one 
test for termination.  This is fairly common, and occurs with 
augmenting recursion as well as tail recursion.  (Our trophy 
winner today only used one test, as the test for an empty
list is not necessary to make this function work.  But adding
this test could eliminate a lot of pointless computation 
under some circumstances.)  Finally, note that there's no 
helping function in this example of tail recursion.  Because 
we can use the argument "integer" as a counter, we don't need 
to introduce any new arguments to be used as variables to keep 
track of intermediate results.  So it's safe to say that we 
don't always need a helping function to get tail recursion going.


List-consing recursion (a type of augmenting recursion)

LISP's "append" function is a very commonly used function.  
It takes two lists as arguments and joins them together:

? (append '(a b) '(c d))
(A B C D)
?

It's not immediately obvious how to implement this...it takes 
a little thought.  Folks just sort of naturally try to go at 
it by getting to the last element of the first list and 
tacking it on to the second one, then somehow going backwards 
along the first list to tack on the previous element, and so 
on.  But we need to take advantage of recursion here, along 
with the fact that as recursion postpones computations, those 
computations actually get performed in the reverse of the 
order in which they were postponed (last-in-first-out or 
LIFO).  Thus, we don't really want to go to the end of the 
first list and work backward---we want to start with the 
first element of the first list and work forward, postponing 
our operations as we go.  Another insight that is useful here 
is to think of some simpler operations that might get us this 
same result, it becomes a little more obvious.  Consider that

? (cons 'a (cons 'b '(c d)))
(A B C D)
?

gets us the same result, and suddenly the light goes on:

(defun my-append (my-list1 my-list2)
  (cond ((null my-list1) my-list2)
        (T (cons (first my-list1)
                 (my-append (rest my-list1) my-list2)))))

What makes this particular template of interest is the nature 
of the augmenting recursion.  The computation being done 
outside of the recursive call is a "cons".  In this case, the 
elements of the first list are being "cons"ed onto the second 
list, resulting in a new structure combining the two original 
structures.  List consing recursion is a commonly-used way of 
constructing new structures from old ones.

That's all for now...more templates next time.


Copyright 1997 by Kurt Eiselt.  All rights reserved.

Last revised: January 21, 1997