CS 2360 - April 9, 1998

Lecture 4 -- Recursion is Your Friend


Here's the recursive function we created at the end of the
previous lecture:

  (defun my-member (input-item input-list)
    (cond ((null input-list) nil)
          ((equal input-item (first input-list)) T)
          (T (my-member input-item (rest input-list)))))

We mentioned then that this function doesn't work quite like
LISP's "member" function.  One way in which this function differs
from the "real one" is that this "my-member" function just
returns T or nil, but "member" tries to provide a little
bit more information.  So when you call

? (member 'c '(a b c d e))

in your LISP system, you'll see

(C D E) 

returned, not nil.  Why?  Because (C D E) is still a non-nil 
value like T, but your program might find the (C D E) sublist
more useful than plain old T.  It's not much harder to return
the sublist...just change T to input-list:

  (defun my-member (input-item input-list)
    (cond ((null input-list) nil)
          ((equal input-item (first input-list)) input-list)
          (T (my-member input-item (rest input-list)))))

The other major way in which "my-member" differs from "member"
is that "my-member" doesn't use the correct equality predicate.
That is, there's more than one way to test for equality in this
language, and "equal" isn't the right one for purposes of mimicing
the behavior of LISP's "member" function....


Equality predicates

There are several equality predicates worth knowing about.  
In "A Programmer's Guide to Common LISP", Deborah G. Tatar 
explains it pretty well (pp. 48-50):

"...there are four important general tests for equality.  
These tests take any two LISP objects as arguments, and check 
to see if they are equal.  Naturally, two objects must be of 
the same type to be equal.

You might wonder why four tests are necessary.  Why doesn't 
one test serve the purpose?  The reason is that there are 
degrees of equality.  Most of the time you want to know 
whether two objects look the same, but sometimes you have to 
know whether they are actually the same object in memory.  
That accounts for two of the tests.  Then, as it turns out, 
minor modifications on each of the major tests make two more 
surprisingly useful functions.

EQUALP and EQUAL are the more general equality predicates.  A 
good rule of thumb is that two objects are EQUALP or EQUAL if 
they look the same when they are printed on the screen.

:
:

The difference between EQUAL and EQUALP is that EQUALP is 
less pure in its definition of equality.  Simply because it 
turns out to be useful, EQUALP ignores differences in case in 
characters and type in numbers.  For example,

(equal 3 3.0)
NIL

but

(equalp 3 3.0)
T

[Or to answer the question that was asked in class today,

? (equal 3/4 0.75)
NIL
? (equalp 3/4 0.75)
T
?  ]

Also,

(equal "YES" "yes")
NIL

(equalp "YES" "yes")
T

The last example demonstrates one of the instances in which 
EQUALP is useful; if you had solicited user input, you 
probably wouldn't care whether it was typed in lower-, or 
uppercase letters, or both.

The other two equality predicates, EQ and EQL, tell you 
whether you are looking at two objects in memory or at one.  
Why do we need operators like these?  Consider the following 
calls and returned values:

(equal (cons 'a 'b) (cons 'a 'b))
T

(equalp (cons 'a 'b) (cons 'a 'b))
T

These might look like good answers, and for many purposes 
they are; however, consider that CONS is a function that 
performs an operation.  Each time you call CONS, a new cons 
cell is constructed.  The contents of two cons cells may be 
the same or look the same but they are separate objects, just 
as twins who have DNA with the same sequence of nucleotides 
are still separate persons.  EQ and EQL test whether two 
objects not only look alike, but whether they are the same, 
that is, located in the same place in memory.  In other 
words,

(eq (cons 'a 'b) (cons 'a 'b))
NIL

(eql (cons 'a 'b) (cons 'a 'b))
NIL

This kind of test is important when you have the ability to 
change objects.  Then you often need to know whether both 
items will change, or only one.

:
:

One characteristic difference between EQ and EQL has to do 
with the way LISP handles numbers.  EQ returns true only if 
two numbers are in exactly the same location in memory.  
Small numbers (called FIXNUMS) have a direct representation 
in memory, and are always EQ.  However, LISP must create a 
representation for very large numbers (BIGNUMS) and for 
floating-point numbers each time they are used.  Therefore, 
they may not be EQ.  It turns out that much of the time you 
won't care about exact identity in that case.  Furthermore, 
the number of fixnums is implementation-dependent.  EQL is 
provided as a portable version of EQ.  For example, in a 
given implementation of LISP:

(eq 1234567890 1234567890)

may return T or NIL, but:

(eql 1234567890 1234567890)

always returns T.

The difference between EQ and EQL is rather subtle; in fact, 
the only reason for introducing EQL at this early stage is 
that it is the default test that LISP functions use to test 
for equality."

In addition, there's yet another useful equality predicate,
which is simply =.  The = predicate takes only numeric arguments;
anything else will cause an error.  It works on numbers of different
type, so that 

(= 4 4) returns T, and

(= 4 4.0) also returns T.

Which equality predicate does LISP's "member" function actually use?
It's EQL, not EQUAL.  In fact, EQL is the default equality predicate
for LISP functions.  So "my-member" would more accurately mimic the
behavior of "member" if it were written like this:

  (defun my-member (input-item input-list)
    (cond ((null input-list) nil)
          ((eql input-item (first input-list)) input-list)
          (T (my-member input-item (rest input-list)))))


Other predicates

While we're on the topic of predicates, remember that a predicate
is a function that returns a "yes" or "no" (non-nil or nil) value.
There are other useful predicates that you should know about.  
For example:

  (atom *expr*)      returns non-nil if *expr* is an atom,
                     nil if *expr* is not an atom

  (numberp *expr*)   returns non-nil if *expr* is a number,
                     nil if *expr* is not a number

  (listp *expr*)     returns non-nil if *expr* is a list,
                     nil if *expr* is not a list

  (symbolp *expr*)   returns non-nil if *expr* is a symbol,
                     nil if *expr* is not a symbol

Historically, many functions designed to work as predicates 
(i.e., returning true/false values) have had the letter "p" 
appended to their names, hence "numberp" and "listp".  
Obviously, folks haven't been too consistent in this, since 
"atom" is not "atomp".  It's quaint idiosyncrasies like this 
that give any language some personality, no?  Sometimes, this 
sort of stuff filters into everyday language use.  For 
example, one LISP hacker might ask if another is interested 
in going to lunch by saying simply "lunchp?"....I guess you 
had to be there.


Recursion

As noted earlier, y'all got that "my-member" function to 
search successive elements of a list through the application of
an important computing concept called "recursion".  Recursion 
essentially means defining something in terms of itself.  A 
function is recursive if it (directly or indirectly) calls 
itself.  A recursive function consists of three parts:

1)  the termination condition, or when to stop
2)  the operation or modification, or what to do to the input
    to move closer to a termination condition
3)  the recursive call itself.

Recursion is a program control mechanism that allows 
repetitive operations without traditional iteration, which 
requires the use of side effects and the maintenance of 
variables as counters or temporary storage places...things 
which add unnecessary complexity.  Using recursion 
effectively requires a different style of thinking, but 
you'll get better at it with practice if you find it 
difficult early on.  Recursion also results in nice, clean, 
compact source code which is often easier to read than the 
iterative equivalents.  A recursive function can also eat up 
lots of memory as it is running, but it doesn't necessarily
have to; we'll see more about this later.


The substitution model of evaluation

We briefly went over how the LISP evaluator works in a previous lecture.  
Let's take a moment and go into this issue in a little bit more depth.  
We won't go into all the details here; we'll leave some of the details 
for the weeks to come.  But this little discussion should give you 
enough understanding to make you dangerous.

Let's say you've created a file with all the LISP code for evaluating the 
quadratic formula that I gave you last week.  Then let's say that you've 
also added your implementation of the function for computing the square 
root of the discriminant, as required by the first homework assignment.
Now you tell the LISP interpreter to evaluate all those defun functions, 
as you were shown in your lab session.  You're ready to compute stuff.  
If you type (pos-root 1 0 -4) at the LISP evaluator, here's what will happen:

? (pos-root 1 0 -4)
2
? 

You can follow that a little bit more closely by telling the evaluator that 
you want to "trace" some of the functions that are being called:

? (trace pos-root pos-numerator sqrt-of-discriminant denominator)
NIL
?

Then when you type (pos-root 1 0 -4), you'll see something like this:

? (pos-root 1 0 -4)
 Calling (POS-ROOT 1 0 -4) 
  Calling (POS-NUMERATOR 1 0 -4) 
   Calling (SQRT-OF-DISCRIMINANT 1 0 -4) 
   SQRT-OF-DISCRIMINANT returned 4
  POS-NUMERATOR returned 4
  Calling (DENOMINATOR 1) 
  DENOMINATOR returned 2
 POS-ROOT returned 2
2

What's really happening is something like this:  When I type the list 
(pos-root 1 0 -4) at the interpreter, LISP assumes I want to invoke the 
function "pos-root".  LISP looks up the definition of "pos-root" and 
extracts that definition.  It then evaluates the arguments, 1, 0, and 
-4, which in this case conveniently evaluate to themselves. Then, 
LISP applies that definition to the evaluated arguments.  This can be 
viewed as a substitution, where:

(pos-root 1 0 -4)

  becomes

(/ (pos-numerator 1 0 -4) (denominator 1 0 -4))

This expression is sent to the evaluator again.  LISP evaluates the arguments, 
but this time the arguments aren't numbers but are instead lists.  So these 
expressions are handed to the evaluator, and LISP treats them as function 
calls.  

The order in which these function calls, or any arguments in this example, 
are evaluated won't be important.  Why?  Because of the functional programming 
constraints we've followed, there are no side-effects.  For example, computing 
pos-numerator in no way affects the computation of denominator and vice-versa, 
so it doesn't matter which one is computed first.  (This will be a very nice 
feature in the parallel-processing world of the future.)  If, on the other 
hand, one of these functions updated some global variable that was accessed 
by the other, we would most definitely worry about which one was computed 
first, resulting in a harder programming job, more complex software, and so on.

Still, something like Macintosh Common LISP has to evaluate one of these 
arguments before the other, and the way it works (as do most LISP systems) is 
to evaluate the arguments left to right.  So LISP evaluates the expression 
(pos-numerator 1 0 -4) and does the appropriate substitution:

(/ (pos-numerator 1 0 -4) (denominator 1 0 -4))

  becomes

(/ (+ (- 0) (sqrt-of-discriminant 1 0 -4)) (denominator 1 0 -4))

This "substitution" process continues until LISP arrives at a solution 
(which requires that all the functions you defined are eventually replaced by 
functions that were already defined in LISP).  In short, the LISP function 
evaluation process can be described like this:

1.  Look up and retrieve the function definition.
2.  Evaluate the arguments to the function (by passing the
    arguments themselves to the LISP evaluation function---
    numbers evaluate to themselves, symbols evaluate to 
    the objects they're bound to, while lists are treated
    as function calls and evaluated accordingly).
3.  Apply the function definition to the evaluated arguments
    (i.e., replace the argument place-holders in the new
    function definition with the corresponding evaluated
    arguments).
4.  Substitute the result of all that for the original
    function call from step 1.
5.  Pass this new expression to the evaluator.

Warning:  The is a very sketchy and hand-wavy description of the evaluation 
process.  It should suffice for now, but as time goes by, we'll beef this 
description up with details about how various types of things get evaluated.  
But rather than dump all the details on you at once, we'll add things 
incrementally as we need them.


Analyzing recursion

Now that you're experienced with recursion in LISP and
the substitution model of evaluation, you can use the
substitution model to analyze the behavior of a recursive
function as it is evaluated.  To do this, we'll introduce
a classic example for 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.  
We can fix this.  Here's how:

(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.  

How did we get that solution?  What makes it so magical?  That's
something for you to think about.  We'll talk more about this on Tuesday.



Copyright 1998 by Kurt Eiselt.  All rights reserved.

Last revised: April 13, 1998