Today's lecture will cover the following topics
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.]
--------------------- | 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 | ---------------------
| 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 0Now, 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.
(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
(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.
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-windowIn 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.
(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 ) 50The 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.
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,
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-CWindowTo 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".
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.
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.
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 ]
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
The precedence list for a class can be computed by traversing the corresponding network as follows: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.
- Start at the bottom of the network.
- Walk upward, always taking the leftmost unexplored branch
- 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.
- 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 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.