[N-World Contents] [Book Contents] [Prev] [Next] [Index]

Lisp (LISt Processing) is one of the most refined of the programming languages in use today. Originally developed in the early 1960s, it has developed into a rich and complex, fully object-oriented language. In fact, it is the only object oriented language with an ANSI standard. Today, Lisp is used in a wide range of applications, from Artificial Intelligence research to computer graphics.

This chapter provides a brief overview of some of the most important Lisp concepts, including:


The Lisp Interpreter and Evaluation

Xemacs includes an interface with the Allegro Common Lisp interpreter. This interface includes, among other things, several Xemacs menu items, as well as the Lisp prompt:

USER(1):

The current Lisp package is shown on the left sign of the prompt, while an incremental counter (similar to many UNIX shell prompts) appears on the right. You enter your Lisp forms to the right of the colon.

You interact with Lisp the Lisp interpreter through the Lisp listener. You supply the listener with forms or symbols for evaluation. Evaluation always produces a result, which is returned by the interpreter. This result is called the returned value. Generally, Lisp forms are evaluated when you hit the return key after typing them into the interpreter. The process of reading input, evaluating symbols, and printing the returned value is called the read-eval-print loop.

For example, consider how to add the numbers 2 and 2. In Lisp you evaluate the form:

USER(2): (+ 2 2)

and Lisp returns the value 4.

USER(2): (+ 2 2)

4

In other words, the form (+ 2 2) evaluates to 4. We can symbolize this relationship with the => symbol, which means "evaluates to.":

(+ 2 2) => 4


Symbols

Symbols are named data objects which serve to name functions and variables within programs. Internally, a symbol is a data structure with five components:

For now, we'll focus on symbol-name and symbol-value. Values are associated with symbols by binding. For example, the setf function binds a symbol to a value, as in this form:

USER(3): (setf x 7)

7

A symbol bound to a value has a symbol-name and a symbol-value. Evaluating a symbol returns the value of that symbol.

USER(4): x

7

However, if you evaluate a symbol with the single-quote sign ('), the symbol-name is returned:

USER(5): 'x

x

Symbols allow you to reference either the symbols themselves, or the values they are bound to. They can be bound to any Lisp data type, including complex data structures, classes, and functions. In addition, all Lisp data objects, including every function and variable defined in N·World, are themselves symbols.


Packages

Packages are collections of Lisp symbols which serve as name spaces. They provide a handy way of preventing name conflicts when several developers are working on different pieces of a large project. Symbols within packages can be declared to be internal or external.

A given package can also be declared to use another package. If package A uses package B, then symbols in package B can be used by functions in package A as if they were internal to A itself. No special syntax is required. However, it is usually prudent to use single-colon syntax to help keep track of which functions are truly internal to a package, and which are defined externally.

Packages can have both names and nicknames. For instance, a given package might have the name Package A, and the nicknames A and PA.


Forms and Evaluation

A Lisp form is an expression, enclosed within parentheses, which returns a value when evaluated by the Lisp interpreter. Forms can themselves contain other forms nested within them. Evaluation occurs from left to right. Evaluation for the form:

(+ 2 3)

can be summarized:

2 => 2

3 => 3

+ with arguments 2 and 3 => 5

Numbers, strings enclosed within double-quotes (") and quoted symbols (e.g. `x , `blivet) evaluate to themselves. Unquoted symbols which have been bound to values (in other words, for which symbol-value is not undefined) evaluate to their values. If a symbol has not been bound to a value, evaluating it results in an error.


Functions

A function is any symbol which can be executed as code when supplied with the appropriate number and types of arguments. Functions accept arguments and return values. For example, the Lisp addition function + operates on arguments you supply:

USER(7): (+ 2 2)

4

This function can be described as "performing the addition function on the arguments two and two returns the value four." Symbols can also be used as arguments in a function, as in this example:

USER(8): (setf x 9)								;Bind the symbol x to the value 9

9								;Lisp returns the symbol value

USER(9): (+ x 1)								;Evaluate + with arguments x and 1

10								;Lisp returns the result

Because functions return values, they can be used as arguments themselves.

USER(10): (+ 1 (+ 2 3))

6

In this example, Lisp evaluates the forms from the inside out. The form (+ 2 3) returns the value 5, which is then supplied as an argument to the second form,
(+ 1 5). Arguments supplied to a function from outside of that function are said to be passed from one function to another. In this example, the inner function passes it's result, 5, to the next function, which accepts it as an argument.


Evaluation, Side Effects,
and Functional Programming

Functional programming is the dominant paradigm for Lisp programmers. Whereas other languages encourage you to use variables to store values, Lisp programmers focus on the return values of their functions. Setting global variables with setf is generally frowned upon. For example, the following function relies on a variable to hold it's result:

(setf x (+ 2 2)) => 4

But the same effect can be achieved without setting the global variable x, e.g.,

(+ 2 2) => 4

One benefit of functional programming in Lisp is interactive testing. In functional code, you can test each function as soon as it's written. If your functions return the values you expect, you can be confident they are correct. Functional programming allows you to to quickly modify existing programs or create new ones.

Defining Your Own Functions

You can define your own functions using the DEFUN macro, which has the following basic syntax:

(defun FuncName (args) (....body of function....))

Note: A Macro is a special Lisp form whose arguments are evaluated slightly differently than the Lisp evaluation rule otherwise requires.

For example, consider a simple function which sums 7 and 2:

USER(11): (defun add9() (+ 2 7)	;Define the function ADD9,
;with no arguments

add9										 ;Lisp returns the name of the
;function

The empty parenthesis after the name of the function indicates that this function accepts no arguments. Try evaluating this function at the Lisp prompt:

USER(12):	 (add9)

9

Lisp returns the result, 9. Now let's define a simple function which can accept arguments.

USER(13): (defun add-two-nums (x y)

					(+ x y))

ADD-TWO-NUMS

Now, when we evaluate this function we have to supply it with two arguments, using the syntax

USER(14): (add-two-nums 3 4)

7

Failing to supply the number of arguments that a function expects to receive results in an error:

USER(15): (add-two-nums 3)

Error: ADD-TWO-NUMS got 1 arg, wanted 2 args.

  [condition type: PROGRAM-ERROR]

You extend Lisp by creating your own functions. You can use functions which you define in exactly the same way that you use standard ANSI Lisp functions.


Lists

A list is a series of linked cells called conses. A cons cell has two parts, called the car and the cdr. The origin of these seemingly obscure terms lies in the earliest days of symbolic programming. Lisp was developed for use on the IBM 704 mainframe (which used vacuum tubes!). This machine's memory was divided into several components, including the address register and the decrement register. Hence, CAR stems from "Contents of Address Register", and CDR from Contents of Decrement Register.

The car is a pointer to some data object. This data can be of any type, including numbers, strings, other lists, or any Lisp data objects. The cdr is pointer to the car of the next cons cell in the list. Lists themselves are written as symbols between parentheses, e.g.:

(Apple Foo Bar 1)

Lists are always terminated by a special symbol called NIL, or the empty list. In other words, the last element of a list always has NIL as it's cdr. NIL and the empty list are symbolized by two empty parentheses, ().

You can visualize a this list as a chain of cons cells like this:

Here is a list which contains a list:

( A (BIG FAT) LIST)

And it's cons cell representation:

Manipulating Lists

Lists are useful for managing sets of data. For instance, you can collect several discrete items into a list and pass them to a function as a single argument. You can also perform operations on each of the elements of a list in sequence, or search through a list and operate on those elements which meet certain criteria. Finally, lists can be useful for managing sequential data sets, since each element of a list has a pointer to the next element in the list in its cdr.

Lists are superficially similar to another Lisp data type called vectors, which are one-dimensional arrays. They differ primarily in that the amount of time necessary to access a component of a list increases with the length of the list, whereas the access time for any element of an array is constant. However, adding an element to the front of a list takes a constant amount of time, whereas adding elements to an array takes time in proportion to the size of the array. Also, elements in an array do not reference other elements in the data structure.

Creating a List

Lists are created in several ways. The list function accepts an unlimited number of arguments, which it returns as the elements of a new list. For example,

USER(16): (list 1 2 3 4 5)

(1 2 3 4 5)

The cons function adds an argument as the first element of a list. For example:

(cons 1 (list 2 3 4 5))

Returns

(1 2 3 4 5)

Lisp programmers sometimes refer to the act of using the cons function as consing. You can append elements to the end of a list using the append function:

(append (list 1 2 3) (list 4 5 6))

Returns

(1 2 3 4 5 6)

You can create a one element list by consing a symbol and NIL, or the empty list.

USER(18):(cons x nil)

or

USER(19):(cons x ())

Returns

(x)

Returning Elements of a List

You can return elements of a list by specifying their position using the nth function. For example, to return the fourth element of a list:

USER(181): (nth 3 (list 1 2 3 4 5))

Returns

4

(The first element of a list is considered to be the 0th element, thus nth 3 is actually the fourth element of the list).

Lists are the foundation of Lisp. Many functions you'll use in N·World return lists, so you'll need to be comfortable using and manipulating lists to program effectively.


Variables

Variables are symbols bound to values. In other words, they are symbols whose symbol-values are not undefined. Values are bound to symbols using the setf function:

USER(182): (setf avariable 3223)

3223

Scope

Once bound, variables remain bound within certain programmatic constraints, called the scope of a variable. Scope is a fairly complex issue, but suffice to say that variables can have global scope or local scope. Global variables, once bound, remain bound within the confines of the program within which they were bound. Usually, variables with local scope are bound only within the confines of the form within which they were bound. For example, variables bound within a function remain bound only within the confines of that function. To use variables like these outside of a macro, you must use setf to bind them globally.

Setting Local Variables

You can use the let macro to create local variable bindings:

(let ((variable binding))

(your-code here))

Variables bound with let are only bound within the let form itself. For example, the variable z is defined in this function only within the confines of the let form::

(defun test-let (x y)

(let ((z (+ x y)))

(format t "z: ~a" z)))

Attempting to return a locally-bound variable outside of its scope is an error:

(defun bad-let (x y)

(let ((z (+ x y)))

(format t "z: ~" z)) ;;; the end of the let form

z) ;;; this z is outside of the let, so is undefined

Types

Unlike other languages, Lisp does not require you to specify type for variables. Lisp takes care of these tasks for you. In a sense, variables are generic with respect to the programmer, since they are all of the same type (or no type). Of course, Lisp must determine the types of arguments passed to primitive functions, so unless you instruct it not to it will attempt to determine the types of your variables. The processor overhead required for this type checking can result in a performance penalty, so you may wish to declare types in certain situations (for more information, refer to Chapter 4 in Common Lisp: The Language (Second Edition).

Nichimen Graphics has addressed the issue of typing in the interest of optimizing code for speed by creating several macros, called anti-generics, which essentially declare types for you in certain situations. To learn more about anti-generics, see "Optimizing Math with Anti-Generic Functions," on page 12-1


Arrays

An array is a data structure whose elements are arranged in some Cartesian coordinate space. The rank of an array is equivalent to the number of dimensions it has. For example, a 5 x 5 element array is said to be of rank 5.

Create arrays with MAKE-ARRAY:

(setf my-array (make-array '(5 5))

#2A((NIL NIL NIL NIL NIL)

    (NIL NIL NIL NIL NIL)

    (NIL NIL NIL NIL NIL)

    (NIL NIL NIL NIL NIL)

    (NIL NIL NIL NIL NIL))

You can find any given point in an array by specifying the coordinates of its storage location with the aref function.

(aref my-array 2 1)

NIL

Finally, you can use setf and aref together to initialize elements of an array:

(setf (aref my-array 3 3) 'foo)

#2A((NIL NIL NIL NIL NIL)

    (NIL NIL NIL NIL NIL)

    (NIL NIL NIL NIL NIL)

    (NIL NIL NIL FOO NIL)

    (NIL NIL NIL NIL NIL))


Classes

A class is a data structure which controls the structure and behavior of other objects, which are it's instances. In other words, a class is a template of sorts, and instances are objects produced according to this template. Classes can inherit properties from other classes, and they can define properties which are inherited by other classes. Given a class, Foo, subclasses of Foo are those classes which inherit properties from Foo, while superclasses are classes from which Foo inherits properties.

Classes inherit two major types of properties:

Slots

Slots are essentially storage spaces in an object which contain one and only one value. Slots can have:

Generic Functions

Generic functions are similar to regular functions, except that they know how to behave based on the nature of the arguments passed to them. For example, the generic function move has several methods, which behave differently when different instances of different classes are passed to them. If you pass a geobody to move, the geometry of that body is modified. If you pass an object, then the object is transformed.

Methods are inherited in the sense that a method defined for a given class will work for all of that classes subclasses. This is a particularly powerful aspect of object oriented programming. It means that you only need to code a given method once.

When you define a method, you are implicitly defining a generic function as well. There are also ways of defining methods automatically when you define a class,. For example, if you define an accessor method for a given slot, Lisp automatically creates the appropriate method. Inheritance of methods works the same way regardless of whether a method is explicitly or implicitly created.

Defining Classes

Let's define a few simple classes to illustrate the way that inheritance, generic functions, slots, and methods work. First, you define a class with the defclass form:

(defclass BIRD

		(sp:name-mixin sp:property-list-mixin)

		((body		:initform nil

				:initarg :body

				:accessor bird-body)

		 (beak	:initform nil

				:initarg :beak

				:accessor bird-beak)))

Now, we'll define a subclass of bird, called song-bird:

(defclass song-bird

    (bird)

  ((song :initform "Bye Bye Birdie"

         :initarg :song

         :accessor bird-song)))

Class song-bird has class bird in its precedence list, which means that instances of song-bird will have the same slots as class bird. This includes not only body and beak, but all the slots defined for the classes in birds precedence list as well, including sp:name-mixin and sp:property-list-mixin!

Class song-bird also includes a new slot, song.

Instancing

Now, we'll make an instance of each of our new classes using the make-instance function.

(setq bird-obj 

     (make-instance bird	:name 'my-bird

                        	:body 'fat

                        	:beak 'long-and-pointy))

Now, use the describe function to examine the object we just created.

USER(35): (describe bird-obj)

#<BIRD MY-BIRD> is an instance of #<STANDARD-CLASS BIRD>:

 The following slots have :INSTANCE allocation:

  PROPERTY-LIST   NIL

  NAME            MY-BIRD

  BEAK            LONG-AND-POINTY

  BODY            FAT

As you can see, all the slots we defined are described here with the values we assigned.

Manipulating Slot Values

Both of the slots we defined for class bird have accessor methods we can use to read and change the values in these slots. For example, to return the value of the body slot in our bird, use the get-body method:

USER(36): (bird-body bird-obj)

FAT

Because class bird is a subclass of sp:name-mixin, we can use accessor functions defined for the slots of this class as well, including one which returns the name of the object:

USER(41): (sp:get-name bird-obj)

MY-BIRD

We can also use accessor functions to change slot values:

USER(73): (setf (bird-body bird-obj) 'a-fat-bird)

A-FAT-BIRD

Now, describing bird-obj reveals the new value for the body slot:

USER(75): (describe bird-obj)

#<BIRD MY-BIRD> is an instance of #<STANDARD-CLASS BIRD>:

 The following slots have :INSTANCE allocation:

  PROPERTY-LIST   NIL

  NAME            MY-BIRD

  BEAK            LONG-AND-POINTY

  BODY            A-FAT-BIRD

Defining Methods

Let's create methods for both our new classes. The following code defines two methods called hit-bird:

(defmethod HIT-BIRD ((self song-bird) new-song) 

  (setf (bird-song self) new-song))

(defmethod HIT-BIRD ((self bird) new-body)

  (setf (bird-body self) new-body))

Defining these methods results in the implicit definition of a generic function with the same name, hit-bird. When called, hit-bird behaves differently depending on the type of object passed it. In the first case, passing an instance of class song-bird and an argument to hit-bird results in the value of the :song slot being replaced by the argument. In the second case, passing an instance of class bird and an argument to hit-bird results in the value of the body slot being changed to the value of the argument. In each case, the methods invoke the accessor functions for the slots in question to accomplish the change.

Inheritance of Slots and Methods

In our examples, song-bird is a subclass of bird. Therefor, methods and slots defined for bird are also defined for song-bird. To illustrate this concept:

1. Create an instance of each class

USER(94): (setq bird-obj (make-instance 'bird))

#<BIRD NIL 435420666>

USER(95): (setq song-bird-obj (make-instance 'song-bird))

#<SONG-BIRD NIL 435437210>

2. Use the :accessor function to change the value of the body slot for bird-obj.

USER(110): (setf (get-body bird-obj) 'a-fat-bird)

A-FAT-BIRD

3. Use the same accessor function to change the value of the body slot for song-bird-obj.

USER(113): (setf (get-body song-bird-obj) 'a-fat-song-bird)

A-FAT-SONG-BIRD

4. Describe each object

USER(121): (describe bird-obj)

#<BIRD NIL 435420666> is an instance of #<STANDARD-CLASS BIRD>:

 The following slots have :INSTANCE allocation:

  PROPERTY-LIST   NIL

  NAME            NIL

  BEAK            NIL

  BODY            A-FAT-BIRD

and for song-bird

USER(122): (describe song-bird-obj)

#<SONG-BIRD NIL 435437210> is an instance of #<STANDARD-CLASS SONG-BIRD>:

 The following slots have :INSTANCE allocation:

  PROPERTY-LIST   NIL

  NAME            NIL

  BEAK            NIL

  BODY            A-FAT-SONG-BIRD

  SONG            "Bye Bye Birdie"


Object Oriented Programming Tools

If you understand how classes are defined, and how they inherit slot values and methods from one another, you're well positioned to find out how to manipulate objects on your own. Franz Allegro Common Lisp includes several powerful tools which will help you to analyze the components of N·World objects. These tools are included in the Franz Allegro Common Lisp composer. For more information about this powerful tool, see "Object Oriented Programming Tools," on page 3-7



[N-World Contents] [Book Contents] [Prev] [Next] [Index]

Another fine product from Nichimen documentation!

Copyright © 1996, Nichimen Graphics Corporation. All rights reserved.