We left off last time with

    (defun quadratic (a b c)
      (list (pos-root a b c)
            (neg-root a b c)))

    (defun pos-root (a b c) 
      (/ (pos-numerator a b c)
         (denominator a)))

    (defun pos-numerator (a b c)
      (+ (- b)
         (sqrt-of-discriminant a b c)))

    (defun denominator (a) 
      (* a
         2))

    (defun neg-root (a b c)
      (/ (neg-numerator a b c)
         (denominator a))) 

    (defun neg-numerator (a b c) 
      (- (- b)
         (sqrt-of-discriminant a b c)))

Dividing it up into all this is "procedural abstraction".

If you go back and look at the LISP code just created, some things should be
obvious.  We generated a lot of procedures (and we didn't even implement the
sqrt-of-discriminant!), but each one of those procedures is very small and
amazingly easy to read.  In fact, I'd argue that someone who had no LISP
exposure whatsoever could figure out pretty quickly exactly what was
intended here, even without any supporting documentation.  And that's another
thing---because we've taken the time to make our function names very
descriptive, we've reduced the need for in-line documentation (although we
haven't eliminated the need, so don't start thinking that you don't have to
document your work!).  Also, because each of these functions are small and
adhere to our functional programming constraints, they're a snap to debug if
something goes wrong.  So while one might want to argue that this approach
creates "many unnecessary procedure calls and is therefore inefficient code"
(we wouldn't want to impose on the computer, would we?) or that it requires
too many unnecessary keystrokes (your fingertips are probably bleeding just
in anticipation of all this typing), one would also be certifiably insane
if one argued that understanding and debugging this more "efficient"
(yet still functional) version:

    (defun quadratic (a b c)
      (list (/ (+ (- b)
                  (sqrt (- (* b b)
                           (* 4.0 a c))))
               (* 2.0 a))
            (/ (- (- b)
                  (sqrt (- (* b b)
                           (* 4.0 a c))))
               (* 2.0 a))))

was in any way easier than understanding and debugging what we created
earlier, wouldn't one?


LISP
~~~~
LISP acts like an interpreted language.  If you know good old fashioned
BASIC, for example, then you're familiar with interpreted languages.
BASIC offers the user the possibility of typing in a well-formed
expression without a line number, and the BASIC interpreter will
execute that expression immediately.  There's no separate compile-and-
load cycle as with Pascal or C.  LISP provides the same kind of easy
interaction that BASIC did (one of BASIC's few, if not its only,
redeeming quality).  That is, you can fire up the LISP system
(you'll learn how to do that in lab), wait for the prompt,
type in a well-formed LISP expression, and LISP will immediately
evaluate that expression and return the result, which is in turn
displayed on your monitor.

Don't be misled however; most LISP systems today are not interpreters, 
but are in fact incremental compilers which produce much speedier code
than an equivalent interpreter could.  But as you look at it from the
outside, the behavior of a LISP incremental compiler doesn't look much
different than a LISP interpreter.  And either way, once you fire
it up, that LISP evaluator is just sitting there, patiently waiting
for you to type something in so that the LISP system can evaluate
it and return the result.  How does it evaluate what you put in
front of it?  Well, it just depends on the nature of the thing you
put in front of it...that is, it depends on the data type.


LISP's simple data types

LISP comes with its own "abstract data types" or ADTs.  As you undoubtedly 
remember from previous courses, an ADT is (1) the logical data structure 
itself (an abstraction, not the detailed implementation), combined with (2) a 
set of operations which work on the data structure.

The two basic data types in LISP are "atoms" and "lists".  An atom is a 
non-divisible thing, like symbols (foo, bar, +) and numbers (42, 3.15).  
They're not exactly interesting, nor are the operations associated with them.

The more interesting data structure is the list.  A list is an ordered set of 
atoms or lists (a recursive definition, no?).  That ordered set must begin 
with a "(" and end with a ")", and it may contain any number of atoms or lists 
in any order, and therefore includes the empty list.  The following are all 
examples of valid lists:

()
(a)
(a b c)
(a (b) c)
(a (b (c)))
(c is useless)
(defun pos-root (a b c) (/ (pos-numerator a b c) (denominator a)))

(Both atoms and lists are often called symbolic expressions, or even just 
expressions, which is why we talked about evaluating expressions occasionally 
in the discussion a few paragraphs above.)

Notice that the very last list up there looks a whole lot like a
function definition.  That's because it IS a function definition.
All function definitions in LISP are lists.  All function invocations
in LISP are lists (and defining a function is nothing more than a
special case of invoking a function, no?).  Which leads us to talk
briefly about one of LISP's special attributes that is shared
with very few other languages: in LISP, there is no distinction
between "program" and "data".  In LISP, you can manipulate the list
that describes how a function is defined with exactly the same
operations that you'd use to manipulate a list containing the names
of all the people you owe money to.  In fact, it's not hard to
write LISP code that generates more LISP code...you can write
programs that write other programs on the fly.  The result isn't
especially easy to debug, but such programs can be useful.

So how does LISP know whether a given list is meant to be viewed
as "data" or as a "program"?  It's all context-dependent.  That is,
it all depends on the context that particular list is embedded in
when the LISP evaluator sees that list.  Here are some simple rules
to help you understand what the LISP evaluator is doing when you type
something at the prompt:

   If you pass a number to the LISP evaluator, the number will
   evaluate to itself.  The evaluator will return that number
   (which will then be displayed on your monitor).

   If you pass a symbol to the LISP evaluator, the symbol will
   evaluate to whatever value the symbol is bound to.  The evaluator 
   will return that value (which will then be displayed on your
   monitor).  If the symbol is unbound, the evaluator will break
   and you'll see an error message.

   If you pass a list to the LISP evaluator, LISP assumes that
   you intend to invoke a function whose name is the first element
   of that list, and whose arguments are the remaining elements in
   that list.  LISP evaluates each of those arguments and retains
   the results.  Then LISP finds the function definition associated
   the function name given as the first element of the list.  Then
   LISP performs the actions given in the function definition on 
   the evaluated arguments and returns the result.  And of course,
   if the first thing in that list really isn't the name of a 
   function, or if the evaluation of the arguments causes a problem,
   then the evaluator will break.

It's really a bit more complex than that, but not too much so.  That
little exposure to the LISP evaluator should suffice for now.  So now
that you have knowledge of the evaluator and LISP's favorite data
structure, the list, what kinds of things can you make the evaluator
do to lists?


Simple operations on lists

There are three fundamental operations on lists: two of them are used for 
decomposing lists and getting at their components, while the other operation 
is used for composing lists.

The first list operator is called "first".  Given an argument which is a list, 
"first" returns the first element of that list.  For example, if the symbol A 
has been bound to the list (X Y Z 4), then (first A) will return X.  Note 
that the original list will not be altered in any way; A is still bound to 
the list (X Y Z 4).

The second list operator is called "rest".  Given an argument which is a list, 
"rest" will return the list consisting of all the original elements of the 
list except the first element.  Thus, assuming A is still bound to (X Y Z 4), 
(rest A) will return (Y Z 4).

The third operator is called "cons", which you can think of as being short for 
"construct".  Given two arguments, where the first is anything and the second 
is a list, "cons" returns the list that you get by inserting the first 
argument as the first element of the list that's the second argument.  
Got it?  So, (cons (first A)(rest A)) returns (X Y Z 4).

Let's go over it again:

What does (first (X Y Z 4)) return?

Time's up.  If you said it returns X, then you're dead wrong.  Why?  
Because you forgot the evaluation rules:

(first A) where A is bound to the list (X Y Z 4) is not the same as 
(first (X Y Z 4))!  

In the first case, the argument A evaluates to the list (X Y Z 4), and then 
when the definition of "first" is applied to that, the first element of that 
list, X, is returned.  Great.  In the second case, however, LISP looks at the 
argument (X Y Z 4) and tries to evaluate that as a call to the function named 
X with the arguments Y, Z, and 4.  If you don't have a previously-defined 
function named X, or if Y or Z aren't bound to something, you'll see LISP 
grind to a screeching halt, as we mentioned above.

In short, anything you throw at LISP, LISP will try to evaluate.  Give it a 
symbol and LISP will try to find what the symbol is bound to.  Give it a list, 
and LISP will try to evaluate it as a function call...


Preventing evaluation

...unless you explicitly tell LISP not to evaluate what you're giving it!  
How do you do that?  With another function, called "quote", which is merely a 
function that takes an argument, doesn't evaluate it, and returns that 
argument.  For example, (quote (X Y Z 4)) returns (X Y Z 4).  So 
(first (quote (X Y Z 4))) returns X, not an error.

The "quote" function is used so often that it gets an abbreviation: the 
apostrophe or single quote mark.  So (quote (X Y Z 4)) is the same thing 
as '(X Y Z 4), and

? (first '(X Y Z 4))
X
? (rest '(X Y Z 4))
(Y Z 4)
? (cons 'X '(Y Z 4))
(X Y Z 4)
?


Lecture notes by Kurt Eiselt, 1998.
Minor changes / additions by Brian McNamara, 1998.
Last updated on Fri Jul 3 02:11:47 EDT 1998 by Brian McNamara