Title: CLOS
1CLOS
- To start off with, CLOS (pronounced kloss or
see-loss) is very different from other object
oriented programming languages - Multiple inheritance is permitted
- Class members are encapsulated, but methods are
defined outside of classes, so encapsulation is
only partial - Information hiding is not enforced
- you can define accessor functions, but there is
an overriding function, slot-value, that permits
access to any objects members - There is no message passing but instead method
evocation through generic functions - These differences are all made available because
of the desire to make inheritance more powerful
than what is available in most other OOPLs - Many people feel that CLOS is not a true object
system whereas others love it
2CLOS as an OOPL
3Recall the Structure
- The original form of object-oriented programming
was provided by the structure - You define the non-homogenous nature by listing
slots - Accessor and constructor functions are generated
as part of the structure definition, and you can
define others - Single inheritance is available by extending a
definition - Inheritance in this case means to inherit
everything, and you are not able to override
previous members (slots) although you can create
more specialized accessor and constructor
functions - As OOPLs became more popular in the mid 1980s, it
was decided that CL needed something more
powerful than structures and so CLOS was created
and then added to the language
4What Does CLOS Give You Then?
- CLOS gives you the ability to utilize multiple
inheritance - This might be considered a good thing by some,
but overly complex by others - There is also an ability to control what gets
inherited - Aside from these two things, CLOS is unlike other
OOPLs for the following reasons - You can dynamically change an objects class
- You can dynamically combine methods without
access to source code - You can dispatch (invoke) based on either class
or object identity - You can dispatch on multiple objects
- You can provide user-defined code to determine
method combination - Additionally, slots can contain data or code so
that invoking a slot of an object carries out an
action
5All Types are Objects in CL
- Ive been purposefully misleading to this point
of the course - It turns out that all of the types that we have
used to date (numbers, characters, strings,
lists, etc) are objects - All objects inherit from T
- One type of object is a standard class
- Classes that you define will inherit from that
class - The class (type) hierarchy is given below
(Standard-Object and Structure-Object have
numerous subclasses, omitted here)
6CLOS Some Basics
- You define classes using defclass
- This is much like defstruct in that you supply
the slots and various specifications for slots - e.g., default values, accessor functions
- Classes may have 0 parents (in which case they
default to inheriting from standard-object), 1
parent, or multiple parents - You define methods using defmethod
- Methods are not necessarily associated with any
give class - Instead, methods are grouped together by common
name where each method is unique in terms of the
type(s) of parameter it expects - you either define a generic function for each
group of methods, or it is generated
automatically - Instead of message passing, a method is invoked
by calling the generic function - the generic function selects which method(s) to
then call - several methods may be called with one invocation
this is where things get complicated!
7Simple Example
(setf b1 (make-instance 'ball)) (setf (slot-value
b1 'color) 'red) (setf (slot-value b1 'type)
'medicine-ball) (setf (slot-value b1 'size)
10) (setf b2 (make-instance 'ball)) (setf
(slot-value b2 'color) 'orange) (setf (slot-value
b2 'type) 'basketball) (setf (slot-value b2
'size) 15)
(defclass ball ( ) (color type size)) (defmethod
getVolume ((b ball)) ( (/ 4 3) pi (cube
(slot-value b 'size)))) (defmethod
getSurfaceArea ((b ball)) ( 4 pi (square
(slot-value b 'size)))) (defmethod getWeight ((b
ball)) (let (weight-external weight-internal)
(if (or (equal (slot-value b 'type)
'medicine-ball) (equal (slot-value b
'type) 'filled)) (setf weight-internal 1)
(setf weight-internal 0)) (setf
weight-external ( (getSurfaceArea b))) (setf
weight-internal ( (getVolume b)
weight-internal)) ( weight-external
weight-internal)))
8Defclass Explored
- Defclass expects three things
- The name of the class
- The parent class(es) in parens empty parens if
you want this class to have no parent - if no parent is specified, then the class has a
default parent of Standard-Object (similar to
inheriting from Object in java) - all slots are inherited from the parent(s)
- methods defined on the parent(s) are also
inherited, well talk about this later - multiple parents are allowed
- the order that you place the parents will be used
to break ties in cases where items (slots,
methods) share the same name between parent
classes - A list of slot names
- as with structures, slot names can have a number
of additional arguments such as default values,
in which case you place the slot name and its
arguments in an extra layer of ( )
9Slot Arguments
- Slots are not protected through information
hiding - To enforce some information hiding, define
accessors - Three types of accessor functions are reader,
writer, and accessor (read or write) - to define accessor functions for a slot, you can
state any or all of these - accessor name, reader name, writer name
- Examples
- (defclass ball ( ) (color (type reader
ball-type) (size accessor ball-size))) - we can get b1s type by doing (ball-type b1)
- or set b2s size by doing (setf (ball-size b2)
12) - but we can only access color by (slot-value b1
color) - Notice the accessor function does not preclude
someone from using slot-value as in (setf
(slot-value b2 ball-size) 0) - Therefore, while accessors give us a way to
define an interface, no object is truly ever
protected since we can always use slot-value!
10More Slot Arguments
- You can also define default values and default
constructors for any slot - Default Initialization initarg symbol
- this allows the caller to place symbol value in
the make-instance instruction so that the slot
will be provided an initial value - usually you will use the slot-name as the symbol
with the name preceded by a as in color or
size - Construction Initialization initform expression
- this provides a default value for the slot,
expression will be evaluated at the time the
object is instantiated through make-instance - if expression is a function call, the function
gets invoked and its return value is used to
initialize the slot - expression will be evaluated only if an initarg
does not exist, or was not supplied in the
make-instance statement - note that the constructor is only for the slot,
not for the entire object - Example
- (defclass ball ( ) ((color initarg color) (type
reader ball-type initform (input-ball-type))
(size accessor ball-size)))
11Methods An Introduction
- The complexity of CLOS lies in the ability to
create a variety of same-named methods - we will start however with simple methods
- Use defmethod to define a method
- (defmethod name (params) body)
- The main difference between a method and a
function is that at least one parameter must
include the type of object that the method is
defined for - assume our ball includes slots to store the ltx,
y, zgt coordinates - the following method might be used to define how
high a ball can be bounced
Notice how one parameter includes a type (ball)
but not all parameters must have them
(defmethod bounce ((b ball) hardness) (let
((btype (ball-type b))) (when (or
(equal btype basketball) (equal btype
tennisball)) (setf (slot-value b z) (
hardness (slot-value b z))))))
12A Constructor Function
- Lets create a method to construct a ball object
for us - What should the constructor do?
- initialize the slots
- Which slots?
- at least the type and size, the initial x, y, z
coordinates may be unknown, so we can use
optional parameters
(defmethod construct-ball ((b ball) type size
optional x y z) (setf (slot-value b 'type)
type) (setf (slot-value b 'size) size) (if z
(progn (setf (slot-value b 'x) x) (setf
(slot-value b 'y) y) (setf (slot-value b 'z)
z)) (progn (setf (slot-value b 'x)
'unknown) (setf (slot-value b 'y)
'unknown) (setf (slot-value b 'z) 'unknown))))
Notice that if z is unknown we change all
coordinates to be unknown If the ball class
would normally default x, y and z to 0, as we
might hope, then this constructor method changes
those default values, probably inappropriately
13An Alternative Constructor
- CLOS does not automatically call a constructor
like in Java when you use new - so if you want a constructor to execute, you must
invoke it explicitly - In the previous version, we already had a ball,
and then we would invoke its constructor by
calling (construct-ball myball ) - here, we have a constructor function (not a
method) that creates a ball and sets up its slot
values
(defun construct-ball (type size optional x y
z) (let ((temp (make-instance
'ball))) (setf (slot-value temp 'type)
type) (setf (slot-value temp 'size) size) (if z
(progn (setf (slot-value temp 'x) x) (setf
(slot-value temp 'y) y) (setf (slot-value temp
'z) z)) (progn (setf (slot-value temp 'x)
'unknown) (setf (slot-value temp 'y)
'unknown) (setf (slot-value temp 'z)
'unknown))) temp))
Of the two approaches, neither is really
similar to Javas constructor
14More on Method Parameters
- When writing a method, at least one of the
parameters must be specialized that is, listed
with its type - The type specialization is what makes a method
unique - Other parameters may or may not be specialized
- in the previous example, hardness is not
specialized and so could take on any type - Consider a situation where you have two sets of
classes - an item to bounce, and a person
- you may want a method based on a type of ball, or
a type of person, or both - below, we have such a case based on whether the
item being bounced is a ball or brick and whether
the person is an athlete, a normal person, or
other
Notice that with two specialized parameters, the
method is no longer defined for a single class
(defmethod bounce ((b ball) (p athlete) hardness)
) (defmethod bounce ((b ball) (p person)
hardness) ) (defmethod bounce ((b brick) p
hardness) )
15Example
(defclass ball ( ) ((type accessor ball-type
initarg type) (size accessor ball-size
initarg size) (x accessor ball-x initarg x
initform 0) (y accessor ball-y initarg y
initform 0) (z accessor ball-z initarg z
initform 0))) (defclass brick ( ) ((x
accessor brick-x initarg x initform 0) (y
accessor brick-y initarg y initform 0) (z
accessor brick-z initarg z initform
0))) (defclass person ( ) ((type accessor
person-type initarg type) (size accessor
person-size initarg size))) (defclass athlete
(person) ((sport accessor athlete-sport
initarg sport)))
16Example Continued
(defmethod bounce ((b ball) (p athlete)
hardness) (cond ((equal (ball-type b)
(athlete-sport p)) (setf (ball-z b) (
(ball-z b) hardness))) (t (setf (ball-z b) (
(ball-z b) .5 hardness))))) (defmethod bounce
((b ball) (p person) hardness) (setf (ball-z b)
( (ball-z b) .3 hardness))) (defmethod bounce
((b brick) (p athlete) hardness) (setf (brick-z
b) ( (brick-z b) .1 hardness))) (defmethod
bounce ((b brick) (p person) hardness) (setf
(brick-z b) 0))
Here, how high something bounces is based not
only on the type of thing (ball vs. brick) but
also based on whether the person bouncing it is
an average person or an athlete
17With-Slots
- Using the accessor function simplifies how to
access slot values, but can still be cumbersome - A short-cut function is with-slots
- Form
- (with-slots (slots) object body)
- Examples
(defmethod increase-size ((b ball) new-size)
(with-slots (size x y z) b (setf x ( x
new-size)) (setf y ( y new-size)) (setf z ( z
new-size)) (setf size new-size))) (defmethod
foobar ((a aclass) (b aclass)) (with-slots
(x y) a (with-slots (z) b (print
(list x y (slot-value a z)) (print
(list (slot-value b x) (slot-value b y) z)))))
18Two Utility Functions
- type-of provide it any datum and it returns the
most specific type - alternatively, you can use class-of
- If you do (class-of (class-of obj)) you get objs
class parent class - find-class when passed the class name (as a
symbol), returns the actual class - this is a pointer to the class definition, which
itself is an object - you could combine this with describe, for
instance, to see a description of the class as in
(describe (find-class ball)) - notice this is different from using describe on
an instance, this enumerates the elements of the
class itself (which contains things that you may
or may not care to see)
gt (find-class 'ball) ltSTANDARD-CLASS BALL
2168C454gt
19Some Notes on Objects
- Recall that in CL, all types are actually
objects, including structs - Here are some restrictions on types of objects
- For built-in-classes (string, number, character,
etc) - you may not use make-instance
- you may not use slot-value
- you may not use defclass to modify
- you may not create subclasses
- For structure-classes (that is, classes created
through defstruct) - you may not use make-instance
- it might work with slot-value (implementation-depe
ndent) - use defstruct to subclass application structure
types - consequences of modifying an existing
structure-class are undefined full recompilation
may be necessary - For standard-classes (defined through defclass)
- none of the above restrictions apply
20Example Shapes
(defclass shape ( ) ((x accessor shape-x
initarg x) (y accessor shape-y initarg y)))
(defmethod move-to ((figure shape) new-x new-y)
(setf (shape-x figure) new-x) (setf (shape-y
figure) new-y)) (defmethod r-move-to ((figure
shape) delta-x delta-y) (setf (shape-x figure)
( delta-x (shape-x figure))) (setf (shape-y
figure) ( delta-y (shape-y figure))))
(defmethod draw ((figure shape))) (defclass
circle (shape) ((radius accessor circle-radius
initarg radius))) (defmethod draw ((figure
circle)) (format t "Drawing a Circle
at(a,a), radius a" (shape-x figure)
(shape-y figure) (circle-radius figure)))
(defmethod set-radius ((figure circle)
new-radius) (setf (circle-radius figure)
new-radius))
21Example Continued
(defclass rectangle (shape) ((width accessor
rectangle-width initarg width) (height
accessor rectangle-height initarg height)))
(defmethod draw ((figure rectangle)) (format
t "Drawing a Rectangle at(a,a), width a,
height a" (shape-x figure) (shape-y figure)
(rectangle-width figure) (rectangle-height
figure))) (defmethod set-width ((figure
rectangle) new-width) (setf (rectangle-width
figure) new-width)) (defmethod set-height
((figure rectangle) new-height) (setf
(rectangle-height figure) new-height)) (defun
polymorph( ) (let ((scribble) (a-rectangle)))
(setf scribble (list (make-instance 'rectangle
x 10 y 20 width 5 height 6)
(make-instance 'circle x 15 y 25 radius
8))) (dolist (a-shape scribble) (draw a-shape)
(r-move-to a-shape 100 100) (draw a-shape))
(setf a-rectangle (make-instance 'rectangle x
0 y 0 width 15 height 15)) (set-width
a-rectangle 30) (draw a-rectangle))
22Generic Functions vs. Methods
- When you define a method whose name had not
previously been defined, CLOS automatically
generates for a you a corresponding generic
function - The generic function is used for bookkeeping in
CLOS - it determines what method(s) to invoke when there
is a method call - You may also write your own generic functions
- (defgeneric name (params) documentation )
- the generic function requires a name and list of
unspecialized parameters - unlike the defmethod in which at least one
parameter must be specialized - there is no body to the generic function
- CLOS sets up a table for the generic function
once it has been defined - as new methods that use the same name are
defined, they are added to the appropriate
generic functions table by listing, for a
specialized parameter list, what method(s) are
invoked - The generic function is necessary for any methods
but you do not have to bother with defining one
yourself
23Example
- From our shape example, when we first define the
draw method, the following generic function was
generated - (defgeneric draw (figure) documentation )
- the generic function draw, had one entry at this
point, ((figure shape)) would invoke that first
defined defmethod - As other draw defmethods were defined, the
generic function was added to - draw ((figure circle)) call the second
definition - draw ((figure rectangle)) call the third
definition - The generic function is consulted every time the
method is called to determine which specific
definition should be invoked - this is how polymorphism is implemented
- what if there is no specific defmethod defined
for this class? - then the generic function finds the closest
matching definition by moving up the class
hierarchy until a class is found that has a
definition - this gets more complicated when we take into
account multiple inheritance
24Inheritance
- In CLOS, inheritance is all-inclusive the child
class inherits all slots from the parent class - further, all methods defined on the parent class
can be invoked by instances of the child class - the generic function selects the most specific
definition, if none are defined for this class,
then the parent class definitions are consulted - In order to override inheritance
- you must redefine things
- you can redefine a slot although it will have
the same name you can change its initform,
initarg or accessor function - accessors are combined (new and old are unioned)
- initargs are combined (new and old are unioned)
- initforms are overridden
- you can redefine a method by including this
class name as the parameter specifier rather
than the parent class
25Multiple Inheritance
- In order to create a class that inherits from
multiple classes, you just list all of the
parents - (defclass multiplechild (parent1 parent2 parent3)
(( more specific slots go here if desired))) - When it comes to inheriting from multiple parents
- slots are inherited from the left-most class
first, and then each successive class to the
right as long as slot names do not conflict - when there is conflict, then only the first slot
of the same name is inherited - the same will be true of inheriting methods the
generic function will select a method based on
the left-to-right listing of each parent class - if none is found, then polymorphism kicks in and
each grandparent is checked from left-to-right
26Example
(defclass creature ( ) ((type accessor
creature-type initform 'unknown) (height
accessor creature-height initform 'unknown)
(weight accessor creature-weight initform
'unknown) (habitat accessor creature-habitat
initform 'unknown))) (defclass bird (creature)
((type initform 'bird) (habitat initform
'air))) (defclass mammal (creature) ((type
initform 'mammal) (habitat initform
'ground))) (defclass ostrich (mammal bird)
((type initform 'ostrich)))
(defmethod lives ((c creature)) (format t "A"
(creature-habitat c))) (defmethod lives ((b
bird)) (format t "I live in the
air")) (defmethod lives ((o ostrich)) (format t
"I am an ostrich")) (lives (make-instance
ostrich)) ? I am an ostrich (lives
(make-instance bird)) ? I live in the air
27What About Multiple Specificiers?
- Imagine that a method had two parameters, both of
which were specialized? - Consider the following partial definitions for
some method op2 - (defmethod op2 ((x number) (y number)) ...)
method 1 - (defmethod op2 ((x integer) (y integer)) ...)
method 2 - (defmethod op2 ((x float) (y number)) ...)
method 3 - (defmethod op2 ((x number) (y float)) ...)
method 4 - Which version is called for each of the
following? - (OP2 11 23) method 2
- (OP2 13 2.9) method 4
- (OP2 8.3 4/5) method 3
- (OP2 5/8 11/3) method 1
- In a left-to-right manner, each parameter is
determined by following the hierarchy, so (OP2 13
2.9) identifies methods with an integer first and
then a float second (method 3 is the only one
that fits integer first, but it does not have
float second) so we try the parent class of
integer (number)
28Class Precedence List
- In order for the generic function to know which
method to invoke, every time a new defmethod
statement is defined, the associated generic
function consults its class precedence list for
the specialized parameter(s) - the CPL is in essence the concatenation of all
classes and their parents of the classes in the
parameter list - consider the figure on the right showing the
class hierarchical structure - C3 is a subclass of C1, C5 is a subclass of both
C3 and C2, etc - The CPL is consulted for each parameter from left
to right until a match is found for all parameters
- Consider a method that accepts an instance of one
of these classes, and a number
- Imagine that we have a method defined with these
specialized params - (C2 Integer)
- (C2 Number)
- (C3 Integer)
- (C3 Number)
- Which method is invoked if we call it with a C6
and an integer? A C6 and a float?
29Calling Multiple Methods
- The strength of the generic function in CLOS is
that multiple methods can be invoked when you
call one - the generic function keeps track of the order by
which to call functions - (call-next-method) is a function that allows a
method to invoke the next one - if we had added (call-next-method) as the last
thing that lives did for the ostrich class, then - lives of ostrich would print I am an ostrich
and then - call lives of the mammal class since there is
none, it would then - call lives of the bird class and we would get I
live in the air - if we had also added (call-next-method) to the
bird class, it would then call lives for the
creature class and we would also get Ground
output - Thus, call-next-method can, in a way, take the
place of super( ) as used in Java to invoke the
parent class method of the same name
30Using Multiple Methods
- Aside from using call-next-method, you can also
create combinations of methods so that, when
called, a group of methods, assembled by the
generic function, will all be invoked, one at a
time - to accomplish this, there are 3 specifiers that
can be placed in a defmethod to denote when the
method should be called - before, after, around
- these dictate when this method should be called
- if a method is available, it is called
- if there is an additional method whose parameters
match, and has before, then it is called first - if there is an additional method whose parameters
match, and has after, it is called last - with these, if there are multiple methods whose
parameters match, then the methods are called
using the CPL like the previous example - if there is an additional method whose parameters
match and has around, then this method, and only
this method, is invoked - You can add call-next-method if you want an
around method to invoke the next around method
31Example
(defclass food ( ) ( )) (defmethod cook before
((f food)) (print "A food is about to be
cooked.")) (defmethod cook after ((f food))
(print "A food has been cooked.")) (defclass
pie (food) ((filling accessor pie-filling
initarg filling initform 'apple))) (defmethod
cook ((p pie)) (print "Cooking a pie.") (setf
(pie-filling p) (list 'cooked (pie-filling
p)))) (defmethod cook before ((p pie)) (print
"A pie is about to be cooked.")) (defmethod cook
after ((p pie)) (print "A pie has been
cooked.")) (setq pie-1 (make-instance 'pie
filling 'apple)) (cook pie-1) "A pie is about
to be cooked." "A food is about to be cooked."
"Cooking a pie." "A food has been cooked." "A
pie has been cooked." (cooked apple)
The return value is from the main method (in this
case, pies non-before/after version of cook)
32Example Continued Using around
(defmethod cook around ((f food)) (print
"Begin around food.") (let ((result
(call-next-method))) (print "End around
food.") result)) (cook pie-1) "Begin around
food." "A pie is about to be cooked." "A food
is about to be cooked." "Cooking a pie." "A
food has been cooked." "A pie has been cooked."
"End around food." (cooked (cooked apple))
Here, the around method uses call-next-method so
that the before, main and after methods are also
invoked Without this however, notice what happens
(defmethod cook around ((f food)) (print
"Begin around food.") (print
"End around food.")) (cook pie-1) "Begin around
food" "End around food"
Why should (cooked ) appear twice?
33Example
- A (slightly) better example is presented here
- We have an adventure game where the base class is
a creature - creatures will have some way to attack and some
way to defend, for now we will use three slots
attack-points, life-points and defense-points - we define an attack method for creature which is
passed another creature - if a random number generated by our creature gt
the other creatures defense-points, then this
creature generates a random number from 1 to
attack-points and this is deducted from the other
creatures life-points - We extend creature to a human class and a dragon
class, which are extended into magician and
magic-negating-dragon respectively - rather than redefining attack for these four
subclasses, we can use some combination of
before, after and around - for instance, if a human is able to perform two
attacks, then a before method might invoke the
parent method twice - the magic-negating-dragon might have an around
method that checks to see if the attacking
creature is using magic, if so, nothing happens,
otherwise this method calls the next method to
permit the attack
34Specializing on an Instance
- Just as parameters in a method specialize on
classes, you can specialize parameters on
specific instances as well - Form
- (defmethod name ((var instance-name) params)
body) - Example
- (defmethod cook ((p my-pie)) (print "My pie is
in the oven.")) - This is of limited use since a programmer may not
know what instances are going to be generated - Consider though an example where the system has a
pre-defined object - if a method is called with any instance, one
behavior might be desired, but if called with
this system object, then another behavior is
desirable - one of the texts gives an example of what happens
when a customer overdraws on their bank account
versus the bank president!
35Code in Slots
- Slots (whether from an object or a struct) just
store what any other CL variable stores, a
pointer - this can point to a number or symbol or list, but
can also point to code - consider a macro that generates code and places
that code into the slots of an object - now, an object-user can invoke the necessary code
by doing (eval (slot-value obj slot-name)) - or alternatively, if the function name itself has
been placed into the slot rather than the
function, you can do (funcall (slot-value obj
slot-name) params) - In this way, you can use macros to generate code
- And you can place that code inside of objects for
encapsulation purposes - encapsulation here is not in the sense of ADTs
but instead in the sense that an object, which is
a problem solving agent, has code available
internally
36One Last Comment
- Because of Common Lisps dynamic nature, CLOS has
the ability to dynamically redefine classes and
instances - if you redefine (or define for the first time)
accessors (including readers and writers) then
you can use them on an instance that was already
created previously - if you redefine or add an initform or initarg, it
has no affect on the previously defined instances - newly added slots are added to the instance
- deleted slots are removed from the instance
(defclass foo ( ) ((a initform '1) (b initarg
b))) (setf f1 (make-instance 'foo b
2)) (describe f1) ltFOO 2069C6B4gt is a FOO A
1 B 2
(defclass foo ( ) ((a initform '3 accessor
get-a) b (c initform '4))) (describe f1) ltFOO
2069C6B4gt is a FOO A 1 B 2 C
4 (get-a f1) returns 1