A Brief Lecture on the Common Lisp Object System (CLOS)

Today's lecture will cover the following topics

Introduction/Motivation

It is the 90's,  why do you want to program with some language created in "Stone Age" of computer science?  All cool modern languages are Object Oriented Languages.  Object Oriented (OO) Analysis/Design/Program are the greatest things since sliced bread.    Lisp isn't OO  so therefore it is out of step with the "modern" age.

Fortunately, that isn't true. The ANSI Common Lisp standard does have an Object System.  The Common Lisp Object  System ( CLOS) makes Lisp a hybird language.  It isn't pure OO. If it were we would have been able to spend all quarter and not mention it.  So you don't have to write code in an OO fashion if you don't want. However, it does have advantages for all of the perhaps overhyped reasons.   In hybird OO languages there is typically a close correspondance between structures/records and classes/objects. So after some notational introductions I'll start from Lisp's structures and evolve the design into a final form that uses most of the primary features of  CLOS.

One of the central themes that have been stressed all quarter in CS2360 has been the utility of using Abstract Data Types (ADTs).   In some sence OO Analysis/Design/Programming is simply lending syntactic support in the constructs used in each phase to support abstraction while taking advantage of reusing code/concepts where possible.

This lecture is going to revolve around designing some OO code that basically corresponds to a window with a label and borders.  [ A derivative of an example outlined in the Keene book referenced at the end of this introduction.]
 

Abstract Data Type Notation

An ADT has to primary components.  I'll be using a graphical notation to talk about the design of the ADT.  In this notation each ADT has a name,   state data/information,  and operations.  In general:
        ---------------------
        | Name              |
        ---------------------
        | state data/info   |
        |                   |
        |-------------------|
        | operations        |
        |                   |
        ---------------------
Some of the state can be retrieved by outside users utilizing the ADT's operations. Some of the state cannot. Portions of the state may be read only, while other aspects can be updated through ADT operations.

For our basic window the ADT might be represented in the following way.

        ---------------------
        | Window            |
        ---------------------
        | x      integer    |
        | y      integer    |
        | height integer    |
        | width  integer    |
        ---------------------
        | window-x          |
        | window-y          | 
        | window-height     |
        | window-width      |
        | window-move       |
        | window-draw       |
        | make-window       | 
        ---------------------

Structures, Slots, and Types

In Lisp the Window ADT could be implemented using structures.   A structure is an aggregate of values. In Lisp terminology each value corresponds to a slot.   The following may be a useful translation table for those familiar with other languages.
 
Language  Aggregate Name Agg. Value Name
 C/C++/Java   Structure/Class  field
  Pascal  Record  field
 Smalltalk  Class instance variable
 Lisp  Structure/Class  slot
 

Slots will hold the state data and functions defined on this structure will provide the operations.  Fortunately, Lisp will do portion of  defining all of the magic for us.      In Lisp structure definitions have the following general format

(defstruct   structure-name     (slot-specification)* )
 slot-specification ==>  ( slot-name  initform  [ :type type-name ] [:read-only  t ] )
So the Window ADT could be delcared as 
                      (defstruct  window 
                                ( x  0  :type integer)
                                ( y  0  :type integer)
                                (height 0 :type integer )
                                (width  0 :type integer ) )
This  declaration gets us all of the slots represented in the ADT notation above and all of the operations execpt for two that are defined below
(defun window-draw  (  a-window ) 
  (check-type  a-window window )
  (format t "upper left ( ~A , ~A ), bounding box  ~A by ~A "
            (window-x a-window)
            (window-y a-window)
            (window-height a-window )
            (window-width  a-window)))

(defun window-move  ( a-window  new-x new-y )
  ;; modest error checking
  (check-type  a-window window )
  (check-type  new-x integer )
  (check-type  new-y integer )
  ;; update the window. 
  (setf (window-x a-window ) new-x )
  (setf (window-y a-window ) new-y )
  a-window )
The above does some type checking.   The defstruct also adds the symbol that corresponds to the structure's name to the type system that Lisp will use.    We could now create a window and try it out
  ? (setf my-window  (make-instance ))
   #S( x 0  y 0 height 0 width 0 )
  ? (setf my-other-window  (make-instance    :height 2 :width 22  :x 1 ))
   #S( x 1  y 0 height 2 width 22)
  ? (window-draw  my-window )
    upper left  ( 0 , 0 ), bounding box  0 by 0
   NIL
  ? (window-move my-window  10 10 )
   #( x 10 y 10 height 0 width 0 )
  ? (window-draw my-window )
    upper left  ( 10 , 10 ), boudning box 0 by 0
Now, let's consider a window with a label.
        ---------------------------
        | Labeled-Window          | 
        ---------------------------
        | x      integer          |
        | y      integer          |
        | height integer          |
        | width  integer          |
        | label  string           |
        ---------------------------
        | labeled-window-x        |
        | labeled-window-y        | 
        | labeled-window-height   |
        | labeled-window-width    |
        | labeled-window-move     |
        | labeled-window-draw     |
        | make-labeled-window     | 
        ---------------------------
 Note that for the most part these are the same slots (and slot accessors ) as Window had.  It would be nice if  there were a why to just reuse that part of our design.  Luckily there is.   There is is what of specifying not just the name of a structure but also options along with the name.  Here's the declaration for labeled-window in Lisp
(defstruct  ( labeled-window (:include window ) )
    (label "New Window" :type string ))


(defun labeled-window-draw  ( a-window )
    (check-type a-window labeled-window )
    (format t "~A~%" (labeled-window-label a-window ))
    (window-draw a-window ))
Not that anything of type LABELED-WINDOW will also be of type WINDOW.  Therefore, not only can we reuse any window ADT operation on something of the labeled-window ADT.  For example, the labeled-window-draw function makes use of the window-draw operation.
 
Similarily let's define a bordered window.
(defstruct  ( bordered-window (:include window ))
    (border-width  10 :type integer ))

(defun bordered-window-draw  ( a-window )
  (check-type a-window bordered-window )
  (format t "upper left ( ~A , ~A ), bounding box  ~A by ~A "
            (window-x a-window)
            (window-y a-window)
            (+ (window-height a-window ) 
               (bordered-window-border-width a-window))
            (+ (window-width  a-window)
               (bordered-window-border-width a-window ))))
The latter is kind of awkward.  The code is predominately full of slot accesses.  Fortunately there is some syntactic sugar that is handy in this situation, WITH-SLOTS.    Basically, this construct defines a new lexical scope in which the slots are bound to identifiers of the same name. [ not quite the total truth but it will do. See a common lisp reference for the additional information on what can be done to these identifiers.]
(defun bordered-window-draw2 ( a-window )
  (check-type a-window bordered-window )
  (with-slots ( x y height width border-width ) a-window 
    (format  t "upper left ( ~A , ~A ), bounding box  ~A by ~A "
                x y (+ height border-width) (+ width border-width))))
This does the same thing as the previous but takes up much less space.

And I'll leave it up to the reader to imagine how a labeled-bordered-window would be defined.   However, there are
couple of problems.  This combination would require yet another draw function.  Additionally given a collection of window objects it would be nice to be able to do something like the following

       (dolist  ( window-instance  window-list )
            (draw  a-window ) )
Namely, invoke  the approriate "draw"  depending upon what type of window each instance was.  In the present
situation we'd have write our own code to descriminate between all of the alternatives.
      (dolist   ( window-instance  window-list ) 
            (case   (type-of  window-instance )
                 (window                      (window-draw  window-instance ))
                 (labeled-window      (labeled-window-draw  window-instance ))
                 (bordered-window     (bordered-window-draw window-instance ))
                 (otherwise           (error "Unknown type of window" ))))
This dispatcher would have to updated for each new "subtype" of window we came up with.  We should make Lisp do this sort of hard work.
 

Object Oriented Programming  (OOP)

 There are three buzz words that are often made use of when talking about OO. We've already seen a little bit of inheritance in action in the previous section.  Basically this typically boils down to not repeating/duplicating aspects of your design in numerous places.  If it located in one place it is easier to find, fix, and maintain correctness than if redundant information were scattered in numerous places.  Polymorphism in simplistic form means we can make it so that one name refers to multiple functions which are selected automagically for us. In other words the solution to the problem illustrated at the end of the previous section.  Encapsulation has to do with adding syntactical/semantic construct so that an ADTs abstraction barrier cannot be violated ( or at least make it difficult) by folks to refuse to "program to an interface, not an implementation" [ see Gang of Four book]
 

Inheritance

In CLOS instead of structures we define classes.  There isn't a huge difference.  The class definition supports more aspects of OOP as I hope to illustrate in remainder of  this introduction.

In a previous CS2360 lecture the concept of a heirarchy was introducted.  Basically classes are types that are composed in a directed acyclic hierarchy.   So for our problem will could have:

                                   T 
                                    |
                                   \ /
                                standard-class
                                    |
                                   \ /
                                window 
                                   /  \
                                  /    \
                                 /      \
                               \ /      \ /
                     labeled-window     bordered-window
In CLOS the root class every class inherits from is named T.   For the level of complexity I will discuss in this introduction all classe will also all descend from STANDARD-CLASS.  [ If you haven't heard of the Meta-Object Protocol (MOP) then don't worry about the difference between the two].

Each instantiations of a class are called instances.   Each instance has all the properties that a member of that class should have.   For instance there could be two instances of the window class

        window1                                    window2 
     ------------------                         --------------------
     | x     0        |                         | x     100         |
     | y     10       |                         | y      20         |
     | height  100    |                         | height 20         |
     | width    50    |                         | width  50         |
     ------------------                         ---------------------
The class WINDOW is the superclass of LABELED-WINDOW and BORDERED-WINDOW in the hierarchy graph above.  Subclass is the reverse of this relationship.  Actually, WINDOW is the direct superclass of these two. This differeniates from  STANDARD-CLASS and T  which are also a superclasses.

So where ready to define a class.  In CLOS class definiation have the following format:

    (defclass  class-name (superclasses* )
                          ( slot-specification* ) 
                          (other-specs)* ) )
     slot-specification ===>  ( slot-name   [:initform form ]
                                             [ :type type-name ]
                                             [ :initarg  keyword-initarg-name]
                                             [ :accessor accessor-name ] )
There are several other optional slot options some of which I'll cover later... and some of which I'll not cover at all.
I'll also not dwell on the other specifictions.   So for the window class
(defclass  Cwindow  ()
   (  (x  :initform 0 
          :type integer
          :accessor x) 
      (y  :initform 0 
          :type integer
          :accessor y) 
      (height :initarg :height
              :initform 0 :type integer 
              :accessor height)
      (width :initarg :width
              :initform 0 :type integer 
              :accessor width))

   (:documentation 
     "The base classs for things window.."))
In the above only the HEIGHT and  WIDTH slots can be supplied as keword optional arguments to the instance creation function/method.

To create an instance of this class

  ? (setf my-cwindow  (make-instance 'cwindow ))
  #<CWINDOW 12322>
  ? (width  my-cwindow )
    0
  ? (setf (y my-cwindow ) 23 )
    23 
  ? ( y my-cwindow )
    23
  ? (setf my-other-cwindow  (make-instance 'cwindow  :width 50 )
  #<CWINDOW 12333> 
  ?  (width my-other-cwindow )
    50
The labled and bordered classes could be declared as:
 (defclass labeled-Cwindow ( Cwindow )
   (  (label  :initform "New Window"
              :type string
              :accessor label) )
   (:documentation 
     "A labeled window."))

(defclass bordered-Cwindow ( Cwindow )
   (  (border-width  :initform 10 
                     :type integer 
                     :accessor border-width ) )
   (:documentation
    "A bordered window."))
 The accessors  X, Y, HEIGHT, and WIDTH all work on instances of these new classes also. Since there are subclasses all the accessors (and methods ) of the superclass are applicable.
 

Polymorphism

In OOP the operations of an ADT are called (in various languages)   messages, member functions, and/or methods. In CLOS the term of choice is methods.  CLOS has a different notion of what a method than most OOP languages.

In most other OOP languages the basic model is that an instance of a object is sent a message.  Other arguments may accompany this message, but in some sense the methods belong to the instance itself.  In the abstract sense they are just as much a part an instance as the slots/fields.

In CLOS the methods are specialized for objects.  Remember that functions are first class datatypes in lisp. So you could perhaps think of specialized methods as specialized functions for the objects that are passed.  In CLOS all of the arguments are signficant in choosing the most specific method to select and run.  In the message passing model the message is always directed at one particular object and the arguments are in some sense along for the ride.

So what we have is a single name upon which multiple specialized functions, methods, are associated.   To declare a name upon which to hang these methods you declare a generic method.    The argument list of a generic method with constrain the interface of the methods that may be associated with that name.  This is know as argument list congruence. Roughly speaking two argument lists are congruent if they have the same number of required arguments and optional arguments. Furthermore, they must all use &key and/or &rest or not use them at all.   It can be rather complicated, but hopefully this brief introduction will do for the examples here.

 The generic method accepts arguments presented to its interface and selects the most specific applicable method.  If there are none an error is signaled.  In general the declaration looks like:

   (defgeneric  generic-name  ( ... arglist ... )  (other-specifications) )
The arglist can basically be what the arglist of any Lisp function could be ( optional and keyword args, etc.). However,
the required parameters can be specialized by defining them as a name type two element list ( e.g.,  (object window) ).

To declare a method and add it to a generic function you basically simply replace DEFUN with DEFMETHOD.
[ You cannot have both a function and a generic method defined upon the same name.  It must be one or the other]
Note that the arglist of a method may ( actually should ) have types associated with the arguments to specialize the method for a particular case.  In general the declaration look like:

   (defmethod  generic-name  aux-method-designation (...arglist... )
             [ declarations | documentation ]
             body )
I'll touch on aux-method-designations in the next section. You can just ignore that for now.

If no generic has been defined for this method the first DEFMETHOD encountered serves dual use as the generic declaration and the definition of the first method.  In general it is best is explicitly declare your generics to avoid any difficulties.

All the slot accessing/setting of a class slot is done through the invocation of methods.

So for our window classe we could define the follow methods:

(defgeneric draw   (  object )   )


(defmethod  draw (   (obj Cwindow ) )
  (with-slots ( x y height width ) obj 
    (format t  "upper left ( ~A , ~A ), bounding box  ~A by ~A "
                x y height width )))

(defmethod draw  (  (obj Labeled-Cwindow ) )
    (format t "~A~%" (label obj ))
    (call-next-method ))


(defmethod  draw (   (obj window ) )
  (with-slots ( x y height width ) obj 
    (format t  "upper left ( ~A , ~A ), bounding box  ~A by ~A "
                x y height width )))


(defmethod move (  (obj Cwindow ) (new-x integer ) (new-y integer) )
   (setf (x obj ) new-x )
   (setf (y obj ) new-y )
   obj)
CALL-NEXT-METHOD probably deserves an explanation.  What this method does is call the next most applicable method.   In some sense there is hierarchy of DRAW methods.
                        default specialization ( an error )
                                   / \
                                    | 
                                CWindow 
                                   / \
                                    |
                                Labeled-CWindow
To find the most applicable you start from the most specific class of the actual OBJ.   If one is at the Labeled-CWindow level the next most specific method is that the one defined upon  CWindow.   This allow you to invoke methods higher in the heirarchy that my may have overridden but still would like to invoke.  Recursvely invoking yourself would be a waste of them since that would invoke the same method as you were currently in.  Even though not explicitly passed, all of the current arguments are passed along to this next "level".
 
 

Encapsulation

 Encapsulation in the OO context means that an object should control access to its internal state.  A programmer who happens to know what the underlying implementation is should be allowed to use operations not in the ADT's interface to violate the abstraction barrier, reach in, and manipulate and/or retrieve state that is outside the bounds of what the ADT's designer/implementor intended.

As is, the classes for WINDOW et. al. allow anyone to change the slots of the classes.   The accessor method allow both read and write access to an instances value.   This doesn't have to be the case. Instead of an accessor you can define a reader and/or writer method for a slot.  The following is a revamped design for our windows.  I've added yet another superclass to WINDOW to illustrate a pointer to be made later.

(defclass  PGadget  () 
   (  (px  :initform 0 
          :type integer
          :reader x  :writer px ) 
      (py  :initform 0 
          :type integer
          :reader y  :writer py)
      (pheight :initarg :height
              :initform 0 :type integer 
              :accessor pheight)
      (pwidth :initarg :width
              :initform 0 :type integer 
              :accessor pwidth))

   (:documentation 
     "The base classs for things with location and bounding box.."))
In the above, slot X and Y have readers and a seperate writer method.   Note also that the name of the slot is now decoupled from the intarg keyword and reader name.   If wished to maintain control over these "P" prefixed slot names and accessors we could simply not export them from the package in which they were defined.   This class and associated methods could be defined inside a package.   In Lisp, the package construct is used to control whether symbols are visible or not outside of the package. 

Since a bounding box is important to gadgets, we'll define a bounding-box method.  Note that because all slot access is through methods there isn't any way to tell outside the abstraction barrier whether this is an actual slot or if the information is synthesized.

(defgeneric bounding-box ( obj )    )

(defmethod bounding-box  (  (obj pgadget ) )
  (list  (pheight obj) (pwidth obj )))
 If bounding-box is exported in the packaging scheme outlined above those outside the barrier would not be able to access the height and width directly.  However, by invoking the bounding-box method be able to indirectly retrieve the information.
(defmethod move    ( (obj pgadget ) (new-x integer) (new-y integer))
  (px new-x obj)
  (py new-y obj)
  obj)


(defmethod draw    ( (obj pgadget ) )
  (format t " Upper Left ( ~A , ~A ) with bounding box ~A "
             (x obj) (y obj) (bounding-box obj )))
In the move method above, the writer methods are used to update the slot values. Not that they are used differently than when the accessor is used in combination with SETF.

[ NOTE:  I've continued to add methods to the MOVE and DRAW generics. The method specialized for the "old" window are still attached.  I didn't have to get rid of them to start this new approach.]

(defclass  PWindow  ( Pgadget )  () )


(defclass  PControlPanel  ( Pgadget ) () )
Now I"m going to have two classes that subclass this general "gadget" class.   In the previous design labeling and bordering a window both involved a subclass with an additional slot.   However, perhaps we would also like to add labels and borders to control panels also.    These "gadgets" classes are our primary class that we wish to hand different "adjectives" onto.

Mixins

There is a legend that long ago ( when Lisp folks were originally thinking of adding OO stuff to Lisp, back in the very early 80's... yes before C++ got rollling ) a couple of folks from the MIT AI lab went to a ice cream shop in Cambridge and were inspired by the technique of adding "mixins" to ice cream.   In this shop a person would pick out the flavor of ice cream they wanted purchase.   There would also be a variety  candies,cookies, and other tasty objects that could be mixed into their selection upon demand (and a few additional pennies, nickels, and dimes).

So lets define a mixin classes.

(defclass  label-Mixin () 
  (  (label  :initform "New Window"
              :type string
              :accessor plabel) )
   (:documentation 
     "The ability to label a gadget."))

(defmethod make-instance ( (obj  (eql 'label-Mixin))
                           &REST CLOS::INITARGS &KEY &ALLOW-OTHER-KEYS)
  (error "An abstract class   ~A cannot be instantiated~%" obj ))

This class should be an abstract class.  An abstract class is a class for which there are no direct instances. Going back to our ice cream shop example you can just order a cup of mixins.  You have to buy the ice cream along with the mixins. These mixin classes should only be instantiated as part of a primary concrete class ( in our case some type of "gadget" class).    By default a MAKE-INSTANCE specialization for LABEL-MIXIN is added to the generic. Howevever, the redefinition above overrides the default method.   The type specialization is different from what we have seen before.  Basically the type specified above is a SINGLETON.  A singleton is a type/class that consists of one uniquely defined instances.  Namely this above function will only be invoked if the argument is precisely the symbol LABEL-MIXIN.  So you can dispatch on individual objects if you wish.
(defmethod  draw  ( (obj label-Mixin ) )
    (format t "~A~%" (plabel obj ))
    (call-next-method))
The order in which the superclasses are specified are significant.  If the mixins preceed the "primary" classes in the superclass list then the above will invoke the mixin method first and then any primary class method.
[ It would invoke an error is there is no "next method".  There is a predicate to test for this if there is some doubt.
  The following examples will be constructed in the correct way so there is no doubt.]

Now that we've done labels it is time to do borders.

(defclass border-Mixin ()
   (  (border-width  :initform 10 
                     :type integer 
                     :accessor pborder-width ) )
   (:documentation
    "The ability to border a gadget."))


(defmethod make-instance ( (obj  (eql 'border-Mixin))
                           &REST CLOS::INITARGS &KEY &ALLOW-OTHER-KEYS)
  (error "An abstract class   ~A cannot be instantiated~%" obj ))


(defmethod  bounding-box ( ( obj border-Mixin ))
   (list  (+ (pheight obj ) (pborder-width obj ))
          (+ (pwidth  obj ) (pborder-width obj )) ))


#|

(defmethod  bounding-box :around ( ( obj border-Mixin ))
   (let  ( (raw-box (call-next-method )) )
     (list  (incf  (first raw-box ) (pborder-width obj ))
            (incf  (second raw-box) (pborder-width obj )))))

#|
The commented method probably takes some explanation.   In addition to being primary methods ( which is the default when you see now aux-method-declaration )   You can define methods that are to be invoked "before" , "after" , and/or "around" a primary method.  The "before" and "after" methods are invoked for their side-effects.  The "around" method may invoke the primary method by invoking "call-next-method". If an "around" method is applicable its value is returned.  Otherwise the value returned is that of the primary method.  Under normal settings for method combination the generic functions invokes [ from Graham's Book ]
  1. The most specific around-method, if there is one.
  2. Otherwise in order.
    1. All before-methods. from most specific to least specific
    2. The most specific primary method
    3. All after-methods, from least spefici to most specific.
In the commented function, I've defined an around method.   Some might take issue with the uncommented version  since it invokes a method on the argument for which there is not applicable method for the class the argument is specified on. This is OK because BORDER-MIXIN is an abstract class and any instantiated object that would be passed to this method would be also be some type of "gadget".  So the code would work.   However, if  someone were to use this mixin with some classs that was not a "gadget" this method would invoke an error.

The around method takes the result of the primary method and updates it.   [ It should probably test for the presence of the next-method for the misuse case outlined above.]  As long as the class BORDER-MIXIN has a bounded-box method defined upon it, this function would work OK... independent of there being any height and/or width slots. Or
even of they did exist but were not exported from their "home" package.  In other words this mixin could actually be in a seperate package.  This allows for it to be encapsulated away from the rest of the "gadget" code.

Now that we have these two mixins we could compose the following heirarchy.

                                                             PGadget 
                                                    |-----------|--------------|
    label-Mixin       border-Mixin                PWindow               PControlPanel
         |               |                          |                        |
         |------------------------------------------------|                  | 
         |               |                          |     |                  /
          \             / \                         |     |                 /
           \   |---------------|--------------------|     |     |----------|
           |   |      /     \  |                          |     |
labeled-borderd-PWindow    bordered-PWindow             labeled-PControlPanel
Every class has an associated precedence list.  This is a serial list of the superclasses from the most specific to the least specific.    Again from Graham the following algorithm
 
The precedence list  for a class can be computed by traversing the corresponding network as follows:
 
  1. Start at the bottom of the network.
  2. Walk upward, always taking the leftmost unexplored branch
  3. If you are about to enter a ndoe and you notice another pah entering the same node from the right, the instead of entering the node, retrace your steps until you get to a node with an unexplored path leading upward. Go back to step 2.
  4. When you ge tto the node representing T, you're done.  The order in which youfirst entered each node determines its place in the precedence list.
This is only really complicated for multiple inheritance.  But is illustrative that the order the superclasses are specified does matter as to what the most applicable methods may be.

This heirarchy would correspond to the following CLOS decrations:

(defclass  bordered-pwindow (  border-Mixin   Pwindow ) 
   () )


(defclass  labeled-PControlPanel (  label-Mixin   PControlPanel ) 
   () )

(defclass  labeled-bounded-pwindow ( label-Mixin border-Mixin Pwindow )
   () )

Well that the end of my brief introduction to CLOS.  This covers the basics, but they are many more interesting things you can do with this object system.  Dispatching on all of the arguments (a.k.a. multi-methods) can a allow very interesting and powerful designs.



References

  1. Object-Oriented Programming in Common Lisp: A Programmer's Guide to CLOS , Sonya E. Keene
  2. ANSI Common Lisp    Paul  Graham
  3. CLtL2   Guy Steele
  4. Design Patterns: Elements of Reusable Object-Oriented Software  Gamma, E., Helm, R., Johnson, R., and Vlissides, J.   (i.e. Gang of Four Book )