CS 2360 - January 8, 1998

Lecture 2 -- Abstraction Action and Function Evaluation


Three important concepts

In writing any computer program, we start with some specification of
a problem to be solved by that program and a set of primitive,
pre-defined operations that can be performed on our computer...that
set of primitive operations constitutes the programming language that
we'll use.  Creating a program that solves the previously-specified
problem using the pre-defined operations in our programming language
involves the repeated application of three different but related
concepts.  These three concepts are:

  abstraction:  giving something a name; treating something 
                complex as if it were simpler; throwing away
                detail.  (Or at least postponing it.)
                We can reduce any complex thing to a simpler 
                thing in this way, and worry about the details later.
                Abstraction includes the notion of decomposition,
                but it's not equivalent to decomposition.

  reference:    mentioning something by name.  This allows us
                to use what was previously abstracted away.

  synthesis:    combining two simple things to make a more
                complex thing; the opposite of abstraction.

(And, of course, those primitive, previously-defined  operations 
are what allow you to stop abstracting.)

These three concepts are essential to controlling the complexity of big 
programs, which is one of the things we'll stress in this course.  In fact, 
the person who wrote one of the LISP books we recommend thinks that 
abstraction is the most important concept in computer science:

    "The most important concept in all of computer science is 
    abstraction.  Computer science deals with information and
    complexity.  We make complexity manageable by judiciously
    reducing it when and where possible.

    "I regret that I cannot recall who remarked that computation
    is the art of carefully throwing away information:  given an
    overwhelming collection of data, you reduce it to a usable
    result by discarding most of its content"

                             Guy Steele


Using these concepts - an example

Consider the quadratic formula that most of you remembered from high school 
math classes (sorry for the crude ASCII representation):

                     ________
                    / 2
            -b +- \/ b  - 4ac
        x = _________________

                  2a

This formula tells us what must be computed in finding the roots or 
x-intercepts of a quadratic expression, but it doesn't exactly tell us how, 
at least if we look at this from the point of view of a computer.  In order 
to get a computer to perform these computations, we're going to have to use 
this formula as a specification for the design and implementation of a program.

The design

How would we design a program to find the roots of a quadratic expression, 
following the constraints imposed by what we already know about the 
functional programming paradigm?  

First, that +- operator tells us that there are two roots, and accordingly we 
can turn this problem of finding the roots into two smaller problems, 
each finding one root.  Right there we've done an abstraction---we've pushed 
some detail out of the way.  We give the big problem a meaningful name, and 
we do the same for the subproblems.  We'll call these problems "quadratic", 
"pos-root", and "neg-root".  Using a little bit of English and a little bit 
of mathematical notation as a design language, we can describe what we've 
done like this:

    quadratic (a,b,c)

    is defined as pos-root (a,b,c)
    combined with neg-root (a,b,c)

     ^             ^
     |             |
     |            here's an example of reference
     |
     |
    here's an example of synthesis

The pos-root problem can be further abstracted.  Essentially, it's a 
division---the result of the numerator divided by the denominator:

    pos-root (a,b,c)

    is defined as numerator (a,b,c)
    divided by    denominator (a,b,c)

(We don't really need the arguments b and c in that denominator function, but 
in the spirit of abstraction, we'll postpone worrying about that detail for 
now.  We did it differently in class.)

To break down the numerator problem, we find that it's the sum of -b and that 
ugly square root, which has a mathematical name:  the discriminant.
Well, actually, it's the thing under the square root symbol that's
called the discriminant, and we'll make sure that's reflected herein.

    numerator (a,b,c)

    is defined as negate (b)
    plus          sqrt-of-discriminant (a,b,c)

What about the denominator?  That's easy:

    denominator (a,b,c)

    is defined as a
    multiplied by 2

As for the sqrt-of-discriminant itself, we've left that as part of your 
first homework assignment.  What about neg-root?  It's pretty much the 
same as pos-root, except that I'm going to have to change numerator so 
that it subtracts instead of adds.  For consistency, I'll go back and 
change the name of numerator to pos-numerator, and I'll change pos-root 
accordingly:

    pos-root (a,b,c)

    is defined as pos-numerator (a,b,c)
    divided by    denominator (a,b,c)


    pos-numerator (a,b,c)

    is defined as negate (b)
    plus          sqrt-of-discriminant (a,b,c)

and then I'll reuse pos-root and pos-numerator with a little bit of adaptation 
to give me neg-root and neg-numerator:

    neg-root (a,b,c)

    is defined as neg-numerator (a,b,c)
    divided by    denominator (a,b,c)


    neg-numerator (a,b,c)

    is defined as negate (b)
    minus         sqrt-of-discriminant (a,b,c)

Unless we want to decompose things like minus and plus, we have pretty much 
abstracted this problem as much as we can.  Since in this course we're going 
to be using Common LISP, we can treat each one of these abstractions as a 
design for a LISP function.


The implementation

How do we turn the design into LISP code?  LISP in its early days was a 
purely functional language and, even though there are lots of things you can 
do in LISP now that do not adhere to functional programming constraints, it 
still retains much of that functional flavor, some of which shows through in 
the language's syntax.  Since we've used a functional notation in our design, 
converting the design to working LISP code will require nothing more than 
some simple cosmetic changes.  We start with this:

    quadratic (a,b,c)
      is defined as pos-root (a,b,c)
      combined with neg-root (a,b,c)

    pos-root (a,b,c)
      is defined as pos-numerator (a,b,c)
      divided by    denominator (a,b,c)

    pos-numerator (a,b,c)
      is defined as negate (b)
      plus          sqrt-of-discriminant (a,b,c)

    denominator (a,b,c)
      is defined as a
      multiplied by 2

    neg-root (a,b,c)
      is defined as neg-numerator (a,b,c)
      divided by    denominator (a,b,c)

    neg-numerator (a,b,c)
      is defined as negate (b)
      minus         sqrt-of-discriminant (a,b,c)


First, we'll get rid of the commas.  LISP doesn't want commas; it regards 
spaces as delimiters.  We only used commas in our design because that comes 
from standard math notation.

    quadratic (a b c)
      is defined as pos-root (a b c)
      combined with neg-root (a b c)

    pos-root (a b c)
      is defined as pos-numerator (a b c)
      divided by    denominator (a b c)

    pos-numerator (a b c)
      is defined as negate (b)
      plus          sqrt-of-discriminant (a b c)

    denominator (a b c)
      is defined as a
      multiplied by 2

    neg-root (a b c)
      is defined as neg-numerator (a b c)
      divided by    denominator (a b c)

    neg-numerator (a b c)
      is defined as negate (b)
      minus         sqrt-of-discriminant (a b c)

The syntax of LISP dictates that all functions begin with a left parenthesis 
and end with a right parenthesis, so let's add those details:

    (quadratic (a b c)
      is defined as pos-root (a b c)
      combined with neg-root (a b c))

    (pos-root (a b c)
      is defined as pos-numerator (a b c)
      divided by    denominator (a b c))

    (pos-numerator (a b c)
      is defined as negate (b)
      plus          sqrt-of-discriminant (a b c))

    (denominator (a b c)
      is defined as a
      multiplied by 2)

    (neg-root (a b c)
      is defined as neg-numerator (a b c)
      divided by    denominator (a b c))

    (neg-numerator (a b c)
      is defined as negate (b)
      minus         sqrt-of-discriminant (a b c))

Note that there are two sorts of things going on in this design.  Sometimes 
we're "defining" functions and other times we're "invoking" them or referring 
to them.  The syntax for these two sorts of things are slightly different, 
for reasons we'll explain in detail as the course progresses.  (The dialect 
of LISP called Scheme is more consistent in this regard.)

To define a new function in LISP, we use a pre-defined function called 
"defun", which stands for "define function".  The syntax for function 
definition is this:

    (defun *function-name* *list-of-arguments* *function-body*)

So now we add the function name "defun" in the appropriate places, and remove 
our English equivalent, "is defined as":

    (defun quadratic (a b c)
      pos-root (a b c)
      combined with neg-root (a b c))

    (defun pos-root (a b c)
      pos-numerator (a b c)
      divided by    denominator (a b c))

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

    (defun denominator (a b c)
      a
      multiplied by 2)

    (defun neg-root (a b c)
      neg-numerator (a b c)
      divided by    denominator (a b c))

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

The syntax for a plain old function invocation is this:

    (*function-name* *one-or-more-arguments*)

The difference in syntax between function definition and function invocation 
has to do with what things get evaluated when, and we'll talk about that in 
the next lecture.  (Actually, function definition using "defun" is just a 
special form of function invocation.)  For the time being, just remember that 
most of the time you'll be using the syntax shown just above.  If we apply 
that syntax to what we've done so far, we get this:

    (defun quadratic (a b c)
      (pos-root a b c)
      combined with (neg-root a b c))

    (defun pos-root (a b c)
      (pos-numerator a b c)
      divided by    (denominator a b c))

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

    (defun denominator (a b c)
      a
      multiplied by 2)

    (defun neg-root (a b c)
      (neg-numerator a b c)
      divided by    (denominator a b c))

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

Now what?  We can get rid of all those English names for arithmetic functions, 
and replace them with the LISP equivalents.  Either of the LISP books will
tell you what those equivalents are.  In case you don't have a book, I'll 
tell you the equivalents:

    plus is just +
    minus is just -
    multiplied by is just *
    divided by is just /

See, I told you it was easy.  But before we do those replacements, remember 
the syntax of a function invocation:  the name of the function comes first, 
followed by the arguments to the function.  It's called prefix notation.  
So we don't just replace those arithmetic operators, we have to move them 
into our prefix form and add the appropriate parentheses:

    (defun quadratic (a b c)
      (pos-root a b c)
      combined with (neg-root a b c))

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

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

    (defun denominator (a b c)
      (* a
         2))

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

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

Almost done.  There's no "negate" function in LISP.  We just use "-" with one 
argument, which LISP interprets as "subtract the argument from zero":

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

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

The last thing we have to deal with is that nebulous "combined with" 
operation.  Functions return a single value, but here we want to return 
two values.  So what we'll do is combine the two numeric values into a 
single data structure and return that.  We'll use LISP's favorite data 
structure, the list.  The way we get LISP to combine a bunch of separate 
entities into a single list is to pass those entities as arguments to a 
function called "list".  So, if we typed (list 2.0 -5.0) to our LISP 
interpreter, LISP would return (2.0 -5.0).  Packaging up our multiple 
numbers into a single structure satisfies LISP's need to return a single 
value as well as our need to see what looks like multiple values returned by 
a single function.  Here's what it all looks like after we've made this last 
alteration:

    (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 b c)))

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

    (defun denominator (a b c)
      (* a
         2))

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

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

That, in gory detail, is a step-by-step illustration of how one goes from a 
design in some off-the-cuff high-level design language to an actual 
implementation in Common LISP.  Initially, we started with a formula, and we 
designed a working implementation of the formula from the top-down by 
breaking off a piece of the problem, designing a solution to that little 
piece, and abstracting away the rest of that problem.  Then we started over 
on the stuff we abstracted away:  we broke off a piece of the remainder, 
designed the solution, abstracted away the rest, and so on.  In fact, if you 
go back and analyze the code, you'll see very little in the way of real LISP, 
and a lot of procedure names we invented on the fly.  We just treated our 
problem and its subproblems as successively refined black boxes, until those 
black boxes mapped directly onto primitive operations that were already 
defined for us.  This is called "procedural abstraction" (which differentiates
it from "data abstraction"---something we'll hear more about later on).

We were aided in this process by thinking "functionally".  Thinking 
"functionally" here means that your procedures access only those values 
that are passed to them as arguments, that they return single values, and 
that they leave no side-effects---that is, a procedure does nothing that 
persists after it returns its value.  That's why we're going to keep you 
away from assignment operations for awhile.

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?


Clarification: "procedure" vs. "function"

Here's some stuff I don't think is worth talking about in class, but
you should know it anyway, just so you're not confused.  In the discussion 
above, I've been using "procedure" and "function" pretty much interchangeably.
This is not uncommon in the LISP programming community.  For example, 
consider this definition by Deborah G. Tatar in "A Programmer's Guide to 
Common LISP":

    "A function is a procedure that obeys the usual rules for
    evaluation."  [You'll find out what those rules are below 
    - Kurt]

Then she goes on to say this:

    "The words 'procedure' and 'operator' are used in this book
    to refer to functions, macros, or special forms.  In LISP, a
    procedure is a thing that you write to express a process.  
    There is no particular opposition between the terms 
    'function' and 'procedure,' since everything in LISP returns 
    a value, and functions usually express a process."

So this implies to me that "procedure" and "function" are interchangeable in 
LISP world (although there are things called "special forms" and "macros" that 
technically aren't functions according to the LISP specification; more about
those in the weeks to come).  That sort of agrees with Winston and Horn in 
"LISP (3rd edition)", who offer these definitions:

    Procedure:  A step by step specification, expressed in a
                programming language, such as LISP, of how to
                do something.

    Function:   Narrowly, a procedure that has no side effects.
                Broadly, any procedure.

Like I said, these folks sort of agree, but not quite.  Tatar says a function 
follows the usual evaluation rules, while Winston and Horn, if we follow the 
"narrow" definition, say something much more restrictive (but desirable)---
namely, that a function has no side effects.  This definition is certainly in 
keeping with the constraints of the functional programming paradigm, but the 
evaluation rules of LISP do not prohibit side effects.  In any case, don't 
lose sleep over it.  The big whammy here comes when we look to people outside 
LISP world for definitions.  For example, Fischer and Grodzinsky in 
"The Anatomy of Programming Languages" say:

    "In semistandard terminology, a function is a program object
    that receives information through a list of arguments, 
    performs a prescribed computation on that information,
    calculates some 'answer,' and returns that value to the 
    calling program....A procedure is just like a function 
    except that it does not return a value."

ARGH!  But they go on to give themselves, and everyone else, a way out:

    "We will use the word 'function' as a generic word to refer
    to functions...and procedures when the distinctions among
    them are not important."

So, in short, it's sort of sloppy. It's ok to call just about anything in 
LISP world a function, as long as there's no need to worry about the technical 
distinctions between things that are functions and things that aren't (e.g., 
macros and special forms).  When you go outside of LISP world, like in CS 3411,
the people there may be a little bit more picky about how you use those words, 
so be careful.


The substitution model of evaluation

Now that you know how to create and run a LISP function, it's time to become 
familiar with how LISP functions are evaluated by a LISP interpreter.  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 in the previous lecture notes.  Then 
let's say that you've also added your implementation of the function for 
computing the square root of the discriminant.  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.



Copyright 1998 by Kurt Eiselt.  All rights reserved.

Last revised: January 10, 1998