Simple object system

Common Lisp includes a comprehensive object system, called CLOS. This article describes how to add a simple object system to uLisp, called ULOS (uLisp Object System), to allow you to take advantage of some of the same benefits it offers in a much simpler way.

For an example of a program based on the simple object system see Mini text adventure game.

Introduction

Object-oriented programming is designed to make it easier to write complex programs that operate on different types of data. It allows you to organise them in a more intuitive and logical way, making them easier to understand, debug, and extend. 

For example, suppose we are writing a program to manipulate shapes. We have two types of objects, rectangles and circles, and the functions rectangle-p and circle-p to test what type an object is. We want to write a function to find the areas of two-dimensional shapes.

The non-object-oriented way to do this would be to write a single function which looked at the type of its argument and behaved accordingly. So, to find the area of an object x we would do:

(defun area (x)
  (cond 
   ((rectangle-p x) (* (height x) (width x)))
   ((circle-p x) (* pi (expt (radius x) 2)))))

The object-oriented approach is to make each object able to calculate its own area. The area function is broken apart and a different version is provided for each type of object.

Using the simple object system

As in CLOS, objects in the ULOS object system are defined as a hierarchy, like a tree, with the most general classes at the top and the more specialised classes further down. For simplicity, in ULOS each object can only have one parent. Objects can either have their own, specific attributes, or they can inherit more general attributes from their parents.

Classes

In our example Shapes program all the objects inherit from a shape class, and square is defined as a special case of rectangle, so the hierarchy looks like this:

Shapes.gif

In ULOS we use the function object to create a new class. It has an optional parameter, the parent. Here are the definitions for the Shapes program's classes:

(defvar shape (object))

(defvar rectangle (object 'shape))
(defvar circle (object 'shape))

(defvar square (object 'rectangle))

Objects

In ULOS there's no distinction between the classes, and the individual objects within each class. So, to make specific instances of a rectangle and circle we use the same function, object. A second optional parameter allows us to define a list of slots and values for the object:

(defvar myrect (object 'rectangle '(width 24 height 10)))

(defvar mycircle (object 'circle '(radius 5)))

Internally objects are represented as an association list of slots and their values:

> myrect
((height . 10) (width . 24) (parent . rectangle))

To get the value of a slot we use the ULOS function value:

> (value myrect 'width)
24

Note that if the object doesn't have the specified slot, value will check its parent, and so on recursively up the tree, until it finds an object with the slot defined.

To update the value of a slot we use the ULOS function update:

> (update myrect 'width 30)
30

The object myrect is now:

> myrect
((height . 10) (width . 30) (parent . rectangle))

The classes can also have slots. For example, if we were cutting the shapes out of plastic we might want to specify a thickness to apply to all shapes:

(defvar shape (object nil '(thickness 10)))

Methods

The last step is to give each class of object the functions for operating on that type of object. In CLOS these are called methods, but in ULOS they are defined using the same syntax as slots.

For example, to extend the definitions of rectangle and circle to include a function area to calculate their own areas we define:

(defvar rectangle
  (object 'shape '(area #'(lambda (x) (* (value x 'height) (value x 'width))))))

(defvar pi (* 2 (asin 1)))

(defvar circle
  (object 'shape '(area #'(lambda (x) (* pi (expt (value x 'radius) 2))))))

Finally, to create a generic area function that works on any type of object we define:

(defun area (obj) (funcall (eval (value obj 'area)) obj))

This gets the object's area function, or searches up the hierarchy for the first object with area defined, and applies it to the object.

Now we can do:

> (area myrect)
300

> (area mycircle)
78.5398

The definition of the object system

The ULOS object system is implemented with three functions.

First object defines an object:

(defun object (&optional parent slots)
  (let ((obj (when parent (list (cons 'parent parent)))))
    (loop
     (when (null slots) (return obj))
     (push (cons (first slots) (second slots)) obj)
     (setq slots (cddr slots)))))

It simply creates and returns an association list containing the definition of parent, and a cons created from each slot-value pair provided in the list slots.

The value function returns the value of a specified slot in an object's association list:

(defun value (obj slot)
  (when (symbolp obj) (setq obj (eval obj)))
  (let ((pair (assoc slot obj)))
    (if pair (cdr pair)
           (let ((p (cdr (assoc 'parent obj))))
             (and p (value p slot))))))

If the slot doesn't exist it searches up the hierarchy of objects by recursively calling value on the object's parent.

Finally update updates the value of an object's slot with a new value:

(defun update (obj slot value)
  (when (symbolp obj) (setq obj (eval obj)))
  (let ((pair (assoc slot obj)))
    (when pair (setf (cdr pair) value)))) 

Here's the definition of ULOS, and the example, in a single file: Simple object system program.

Summary

Why is the object-oriented approach better than the simple approach at the start of this section, using defun and a cond to check the different possible types of object?

  • The object-oriented approach keeps the information about how to deal with each type of object in the method for that type of object, rather than having it in one large function that has to deal with all types of object.
  • If at a later date you want to make your program handle a new type of object you simply have to add an appropriate class and methods, without needing to edit your existing methods.
  • Slots are a convenient way of storing all the information about particular instances of a class.
  • Because classes are hierarchical you can provide a single method that works on several classes in the hierarchy. For example, as defined above area will automatically work on objects of type square because they are a subclass of rectangle.

ULOS was inspired by the chapter "Object-Oriented Lisp" in Paul Graham's wonderful book "On Lisp" [1].


  1. ^ Graham, Paul "ANSI Common Lisp"  Prentice-Hall, New Jersey, 1994, pp. 348-379, available online at http://www.paulgraham.com/onlisptext.html.