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.
Using "cond" -- an example
Let's say we want to define a function which tells us if a
given item is an element of a given list. This turns out to
be a very useful function, and it already exists in Common
LISP. It's called "member". But even though it already
exists, we want the practice, so we're going to construct our
own version. And to make sure we don't inadvertently replace
LISP's version with our own possibly buggy version, we'll
give ours a distinctive name. Following a tradition handed
down through generations of programming courses, we'll use
the convention of creating these distinctive names by taking
the name of the LISP function we're trying to mimic and adding
the prefix "my-" to it. Thus we generate the name "my-member"
for our own version of "member".
What will the design look like? We can sketch it out with a
combination of the LISP syntax we already know, and some
English where we're not sure about the LISP yet. Here's the
first cut:
(defun my-member (input-item input-list)
if done then return "no"
else if input-item = first element of input-list
then return "yes"
else what? see if input-item = next thing on input-list?
how? )
OK, so how are we going to turn all that "if-then-else" stuff into a "cond"?
(defun my-member (input-item input-list)
(cond (done then return "no")
(input-item = first element of input-list
then return "yes")
(what? see if input-item = next thing on
input-list? how? ) ) )
Hmmm. That looks a little more like LISP, but it sure won't
run on my Macintosh. What looks like something that's going
to be real easy to turn into LISP? How about that test to
see if input-item is the same as the first element of input-
list? That should be easy. Just remember the "cond" syntax:
(defun my-member (input-item input-list)
(cond (done then return "no")
((eql input-item (first input-list))
then return "yes")
(what? see if input-item = next thing on
input-list? how? ) ) )
And how do we return "yes" in that case?
(defun my-member (input-item input-list)
(cond (done then return "no")
((eql input-item (first input-list)) T)
(what? see if input-item = next thing on
input-list? how? ) ) )
Nothing to it. How are we going to test if we're done?
Well, if we just sort of walk along input-list, testing the
individual elements to see if they match input-item, what
would be the termination point? When we run out of input-
list, or, in other words, when input-list is nil. So now we
can translate more English into LISP:
(defun my-member (input-item input-list)
(cond ((null input-list) nil)
((eql input-item (first input-list)) T)
(what? see if input-item = next thing on
input-list? how? ) ) )
Wow. Now I have more LISP than English. But there's still
one missing chunk. How do I get this thing to repeat for
every element of input-list (or at least until I match input-
item)? If we were piddling around with Pascal or C, we'd want
to create some sort of loop structure, and maybe create a
variable or two, and throw in an assignment operation here
and there...make it really complicated, and in the process
make ourselves feel good about how much mastery we have over
our computer. Grrrrr.
Well, that's not gonna happen here. Not today at least.
We're going to use a very elegant and computationally pure
form of iteration which LISP supports very nicely. It's
called recursion.
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.
Let's go back now and finish "my-member". What do we want to
do? With "my-member", we're trying to build a function which
does some operation on all the elements of a list, until we
find a specific element. If we're thinking recursively, we
want to break this up into a couple of smaller problems
(there's that abstraction thing again):
1) performing that operation of one element of the list,
combined somehow with...
2) calling the function just defined on the remainder of the
list
So let's apply all this thinking about recursion to "my-
member". So far, we've already coded two different
termination conditions: stopping when we get to the end of
input-list without finding a match, and stopping when we find
a match with input-item. And the test to see if we find a
match between input-item and the first element of input-list
is effectively the "performing that operation of one element
of the list" that we just mentioned. But if neither of those
conditions is true, what do we want to do? We want to call
"my-member" on the remainder of input-list, since that will
get our matching operation performed on the next element of
the list, while at the same time reducing the size of input-
list and thereby getting us closer to a termination
condition. The end result looks like this:
(defun my-member (input-item input-list)
(cond ((null input-list) nil)
((eql input-item (first input-list)) T)
(T (my-member input-item (rest input-list)))))
Oh, one other thing. When "my-member" finds a match, it
returns T. But when Common LISP's "member" function returns
a match, it returns that part of input-list which begins with
input-item. That's also a non-nil result, so it has the same
Boolean value, but it gives us more information than just
"true" or "false". You'll find that LISP tries to do that a
lot, and you should think about doing it too when you can.
To make "my-member" work that way, it would be changed to
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)))))
Analyzing recursion
We've introduced the conditional and an assortment of
predicates. With these tools you were able to invent
recursion by implementing the "my-member" function.
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.
We can fix this. Here's a clue to how we'll do it:
(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))))
We'll talk more about this on Tuesday.
Copyright 1998 by Kurt Eiselt. All rights reserved.
Last revised: January 19, 1998