About me

About me

Feeds

RSS feed

Object-oriented programming

1st May 2018

How do you explain the benefits of object-oriented programming to a beginner without getting into the details of CLOS?

Here's my attempt to come up with a short compelling example showing how object-oriented programming simplifies the writing of programs in Lisp:

Introduction

Object-oriented programming allows you to organise programs in a more logical way. Instead of having a single program which operates on an inert mass of data, the data itself is told how to behave, and the program is implicit in the interactions of these new data “objects.”

For example, suppose we have two types of objects, rectangles and circles, and the functions rectangle-p and circle-p to test what type an object is.

Now suppose we want to write a program to find the areas of two-dimensional shapes.

The old 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)))))

Methods

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. These different versions of a function are called methods.

In Lisp methods are defined using defmethod, rather than defun. So the equivalent object-oriented definitions for area might be:

(defmethod area ((x rectangle))
  (* (height x) (width x)))

(defmethod area ((x circle))
  (* pi (expt (radius x) 2)))

Like defun, in defmethod the function name is followed by a list of arguments, but any argument can be a list:

(argument type)

to specify that the function is only called if the argument is of that type.

When we call:

(area obj)

the correct method will automatically be chosen by CLOS according to what type of object obj is.

Classes

In object-oriented programming we define the type of each object using classes. In our simple example we have just two types of object, rectangles and circles. We could define these as follows:

(defclass rectangle ())
(defclass circle ())

The argument of defclass is the class that this class inherits from. In their most general form classes can form a hierarchy, like a tree, with the most general classes at the top and the more specialised classes further down. So we might make all the classes used by our program inherit from a shape class, and we might include square as a special case of rectangle:

(defclass shape ())

(defclass rectangle (shape))
(defclass circle (shape))

(defclass square (rectangle))

The defclass definition can also specify slots, which are variables associated with each instance of an object. For example, our rectangle objects could have a height and width, and our circle objects could have a radius:

(defclass rectangle (shape)
  (width height))

(defclass circle (shape)
  (radius))

Making objects

Finally, when we want to make a new object we use make-instance to make an instance of the appropriate class, a bit like peeling a sheet off a pad of identical forms:

(setq myrect (make-instance 'rectangle :width 24 :height 10))

(Note that we would have to make the defclass definitions a bit more complicated for this to work as shown in practice).

Each instance of rectangle has its own separate width and height slots.

Now we can do:

> (area myrect)

and we'll get:

240

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.

Full example

Here's a full working example based on the methods and classes described above. You can run this in LispWorks to see how it works.

Suppose we are a manufacturer of perspex shapes for children.

First we define the classes. The perspex comes in different thicknesses, so we give the shape class a thickness slot:

(defclass shape ()
  ((thickness :initarg :thickness :accessor thickness)))

This definition of the thickness slot has a few extra declarations:

  • The :initarg keyword says that we can initialise the slot to a value in make-instance with the :thickness keyword.
  • The :accessor keyword says that we can access the slot with the function thickness.

Now we define the actual shapes, based on this shape class:

(defclass rectangle (shape)
  ((width :initarg :width :accessor width)
   (height :initarg :height :accessor height)))

(defclass circle (shape)
  ((radius :initarg :radius :accessor radius)))

Because rectangle and circle are both based on shape, they each automatically inherit a thickness slot from shape.

Finally we define the methods:

(defmethod area ((x rectangle))
  (* (height x) (width x)))

(defmethod area ((x circle))
  (* pi (expt (radius x) 2)))

The first method is specialised on rectangle; in other words, it only gets called if x is a rectangle. The second method is specialised on circle.

Now you can create an object as an instance of one of these classes:

(setq myshape (make-instance 'circle :radius 12 :thickness 2))

You can read its radius with:

(radius myshape)

or change its thickness with:

(setf (thickness myshape) 3)

and you can find its area with:

(area myshape)

Suppose we want to calculate the weight of one of our perspex shapes. We can define a method on shape which will work for all shapes that inherit from the shape class:

(defparameter *density* 2)

(defmethod weight ((x shape))
  (* (area x) (thickness x) *density*))

Now we can find the shape's weight with:

(weight myshape)

Exercises

1. Define a class triangle with slots base and height.

Add an area method for triangle objects, and check it gives the correct weight for a triangle defined as follows:

(setq mytriangle (make-instance 'triangle :base 12 :height 6 :thickness 1))

2. Define perimeter methods for rectangle and circle objects that returns the length of each object's perimeter.


blog comments powered by Disqus