Last time we'd just finished an augmenting recursion version of
substitute.  Now we'll write the tail-recursive counterpart.

(defun my-substitute (new old input-list)
  (reverse (my-substitute-helper new old input-list nil)))

(defun my-substitute-helper (new old input-list result)
  (cond ((null input-list) result)
        ((eql old (first input-list))
         (my-substitute-helper new old
                               (rest input-list) (cons new result)))
        (T (my-substitute-helper new old (rest input-list)
                                 (cons (first input-list) result)))))


But "my-substitute" is truly tail-recursive only if "reverse" is
tail-recursive.  Here's a tail-recursive "reverse":

(defun my-reverse (input-list)
  (my-reverse-helper input-list nil))

(defun my-reverse-helper (input-list result)
  (cond ((null input-list) result)
        (T (my-reverse-helper (rest input-list)
                              (cons (first input-list) result)))))


What if we want to write append?  It appends two lists:

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

Looking at this version of "append", you can see that appending something
onto the end of a long (or growing) list is computationally expensive.
You have to "cons" together a copy of the first list before you can
run a pointer from the end of that copy to the second list.  So if
you have a choice of building a list by consing elements to the front
and reversing the list once versus appending elements to the end,
make sure you use the cons-and-reverse approach.  It's not just a little
savings in both time and cons cells, it's a big big big (potentially
exponential) savings in time and cons cells.  And it has nothing to do
with whether "append" is implemented with augmenting recursion or tail
recursion (that's about stack space, not time or cons cells).  And how
would you make a tail-recursive version of "append"?  That's part of
your homework assignment.


Just how expensive is it to continually append stuff rather than cons a
list and reverse it?  Very expensive.

Let's take our substitute function and re-write it with append instead of
cons.  That is,

(defun my-ugly-substitute (new old input-list)
  (my-ugly-substitute-helper new old input-list nil))

(defun my-ugly-substitute-helper (new old input-list result)
  (cond ((null input-list) result)
        ((eql old (first input-list))
         (my-ugly-substitute-helper new old
                             (rest input-list) (append result (list new))))
        (T (my-ugly-substitute-helper new old (rest input-list)
                             (append result (list (first input-list)))))))


Simple enough, right?  I did some tests this morning to compare the
efficiency of the two for a rough estimate:

On a 100 element list:

 - my-substitute took 0.02 seconds and 21k of memory. 
 - my-ugly-substitute took 0.04 seconds and 180k of memory.  Which is
   quite a bit more, but we might think we can deal with it.

Now I make the problem 5 times bigger: on a 500 element list:

 - my-substitute took 0.09 seconds and 105k of memory.  Five times the
   memory requirements and run-time; about what we'd expect.

But look at the appending version:

 - my-ugly-substitute took 3.06 seconds and 7.7 _megs_ of memory.  Wow!
   Those numbers got about 70x bigger for a list 5x bigger.  Bad bad bad.

On a 1000-element list,

 - my-substitute took 0.18 seconds and 209k of memory.
 - my-ugly-substitute blew up after a minute or two.

The moral to this story is that simple little decisions like using
append instead of cons can cost you lots of time and lots of memory.
Whenever you're given a choice between constructing a list
element-by-element with cons, and then reversing the final result,
versus appending each new element to the end of the list, you should
always used the cons-and-reverse approach for the efficiency reasons
suggested above.


Multiple recursion

The next function we'll construct isn't already defined in 
Common LISP, but most good LISP hackers have one of these 
sitting around in their personal library of LISP functions, 
or they can at least reconstruct it on demand.  I call the 
function "flatten", and its purpose is to take any list, 
nested arbitrarily deep, and return a simple list of all the 
atoms in the original list.  In other words, it takes a
hierarchical list structure and returns a linear list 
structure, with the order of the atoms intact.  Or, in still 
other words, it removes all the parentheses except the 
outermost ones:

? (flatten '(a (b (c) d (e) f)))
(A B C D E F)
?

   An aside...
   
   People seem to always want to use flatten to fix solutions to their
   problems.  For example, often times when people are constructing a list
   for one of the hw2 problems, they end up with nested lists they didn't
   want.  And they've seen flatten, so they try and use that to fix their
   results.
   
   Aside from the fact that, 95% of the time, the solution with flatten
   still doesn't work right anyway, I want to point out that this is _not_
   a good way to go about constructing a solution.  Don't create a solution
   that's kinda almost half right and then try and fix it at the end by
   calling flatten on it.  Doing so reminds me of those cartoons where the
   character tries to put a square peg in a round hole using a hammer.  
   
   Another similar example from homework 1: the "ideal-weight" problem
   wanted a decimal answer rather than a fraction.  Some people had their
   functions deal with fractions and then at the end took the result and
   added 0.0 to it to make it a decimal.  This works, but again, it's like
   constructing a bad solution and then trying to fix it at the end.  In
   the case of ideal-weight, if you made your constant 704.0 (or whatever
   it was) rather than 704, things worked out peachy from the get-go.
   
   The special case of creating a backwards list and then reversing it at
   the end is acceptable merely by virtue of the efficiency.  But in
   general, don't create solutions with the wrong structure and then try
   and fix them in post-processing.  Build the right things from the
   start.
   
Back to flatten.

How the heck are you supposed to make flatten work?  Well, 
the trick is to use abstraction to break the problem into 
three smaller problems: flattening the first element of the 
list (which could be an atom or a list) into a linear list, 
flattening the rest of the list into a linear list, and then 
combining the results into a single linear list.  We already 
know that the way we join two lists into a single list is by 
using "append", so that problem is solved, and the other two 
problems are handled with recursive calls to "flatten", so 
the whole thing turns out to be pretty easy.  Maybe the hardest 
part is figuring out what the termination conditions are.  Here's
one way to do it:

(defun flatten (mylist)
  (cond ((null mylist) nil)
        ((listp (first mylist)) (append (flatten (first mylist))
                                        (flatten (rest mylist))))
        (t (cons (first mylist) (flatten (rest mylist))))))

The first test condition was pretty straightforward.  If "flatten" 
is passed an empty list, it just returns the empty list and
terminates.  There's really not anything else to do, and that 
empty list will just be "append"ed onto other lists, resulting 
in no change whatsoever.  In the second test condition, we test
to see if the first element of the input list is itself a list.
If so, we flatten that list, and since the result of flattening
is also a list, we can append the result of flattening the first
element of the list to the result of flattening the rest of the
list.  In the final case, if the first element proved not to be
a list, then it must be an atom.  Then we just cons that atom
onto the front of the result of flattening the rest of the list.

It doesn't seem nearly as difficult now, does it?  Once you
start to believe that recursion works without a whole lot
of attention to it, then the solutions to these little problems
become a whole lot more intuitive.  But again, it takes practice...
lots and lots of practice.  And that's why your homeworks are 
soooooo big.

Here's another version that I tend to think of when I think
of flattening something:

(defun flatten (my-list)
  (cond ((null my-list) nil)
        ((atom my-list) (list my-list))
        (T (append (flatten (first my-list))
                   (flatten (rest my-list))))))

It's the same as the one above, except we've just reversed the
second and third conditions.

The type of recursion that's going on here has several names.  
It can be called "multiple recursion" or "tree recursion", or 
it can be called "car/cdr recursion", which is a special kind 
of multiple or tree recursion.

"Multiple recursion" is an appropriate name because, from the 
point of view of our substitution model of evaluation, each 
call to "flatten" is substituted on the program stack by 
multiple calls to "flatten".  

"Tree recursion" is an appropriate name if we think about how 
the original problem is broken down:  we start by trying to 
flatten the original list; that problem becomes two problems 
of trying to flatten two parts of that list; each of those 
problems spins off two more problems, and so on.  If you were 
to sit down and draw a little graph of how the original 
problem was divided into two subproblems, and each of those 
was further divided, and on and on, you'd get yourself a nice 
little tree-like structure.  (Also, note that a hierarchical 
list structure can be interpreted as a tree structure, and 
that "flatten" can be viewed as a preorder tree traversal 
procedure.  But we'll see more on that later.)  

Finally, "car/cdr recursion" is appropriate in this 
particular use of tree recursion because the initial problem 
is broken down into recursive calls of "flatten" on the 
"first" (or "car") of the list and the "rest" or ("cdr") of 
the list.  Today, we might be better off calling it 
"first/rest" recursion, but we'd lose that sense of history 
and tradition.

The important thing to remember here is that multiple or tree 
recursion is a very standard way to travel around really 
complex data structures, or to solve some problems with 
really complex decompositions.  We'll be seeing this 
recursion template pop up in lots of different places.



Lecture notes by Kurt Eiselt, 1998.
Minor changes / additions by Brian McNamara, 1998.
Last updated on Fri Jul 10 22:56:39 EDT 1998 by Brian McNamara