title |
---|
Fundamentals of CLOS |
CLOS is the "Common Lisp Object System", arguably one of the most powerful object systems available in any language.
Some of its features include:
- it is dynamic, making it a joy to work with in a Lisp REPL. For example, changing a class definition will update the existing objects, given certain rules which we have control upon.
- it supports multiple dispatch and multiple inheritance,
- it is different from most object systems in that class and method definitions are not tied together,
- it has excellent introspection capabilities,
- it is provided by a meta-object protocol, which provides a standard interface to the CLOS, and can be used to create new object systems.
The functionality belonging to this name was added to the Common Lisp language between the publication of Steele's first edition of "Common Lisp, the Language" in 1984 and the formalization of the language as an ANSI standard ten years later.
This page aims to give a good understanding of how to use CLOS, but only a brief introduction to the MOP.
To learn the subjects in depth, you will need two books:
- Object-Oriented Programming in Common Lisp: a Programmer's Guide to CLOS, by Sonya Keene,
- the Art of the Metaobject Protocol, by Gregor Kiczales, Jim des Rivières et al.
But see also
- the introduction in Practical Common Lisp (online), by Peter Seibel.
- Common Lisp, the Language
- and for reference, the complete CLOS-MOP specifications.
Let's dive in with an example showing class definition, creation of objects, slot access, methods specialized for a given class, and inheritance.
(defclass person ()
((name
:initarg :name
:accessor name)
(lisper
:initform nil
:accessor lisper)))
;; => #<STANDARD-CLASS PERSON>
(defvar p1 (make-instance 'person :name "me" ))
;; ^^^^ initarg
;; => #<PERSON {1006234593}>
(name p1)
;;^^^ accessor
;; => "me"
(lisper p1)
;; => nil
;; ^^ initform (slot unbound by default)
(setf (lisper p1) t)
(defclass child (person)
())
(defclass child (person)
((can-walk-p
:accessor can-walk-p
:initform t)))
;; #<STANDARD-CLASS CHILD>
(can-walk-p (make-instance 'child))
;; T
The macro used for defining new data types in CLOS is defclass
.
We used it like this:
(defclass person ()
((name
:initarg :name
:accessor name)
(lisper
:initform nil
:accessor lisper)))
This gives us a CLOS type (or class) called person
and two slots,
named name
and lisper
.
(class-of p1)
#<STANDARD-CLASS PERSON>
(type-of p1)
PERSON
The general form of defclass
is:
(defclass <class-name> (list of super classes)
((slot-1
:slot-option slot-argument)
(slot-2, etc))
(:optional-class-option
:another-optional-class-option))
So, our person
class doesn't explicitly inherit from another class
(it gets the empty parentheses ()
). However it still inherits by default from
the class t
and from standard-object
. See below under
"inheritance".
We could write a minimal class definition without slot options like this:
(defclass point ()
(x y z))
or even without slot specifiers: (defclass point () ())
.
We create instances of a class with make-instance
:
(defvar p1 (make-instance 'person :name "me" ))
It is generally good practice to define a constructor:
(defun make-person (name &key lisper)
(make-instance 'person :name name :lisper lisper))
This has the direct advantage that you can control the required arguments. You should now export the constructor from your package and not the class itself.
The function to access any slot anytime is (slot-value <object> <slot-name>)
.
Given our point
class above, which didn't define any slot accessors:
(defvar pt (make-instance 'point))
(inspect pt)
The object is a STANDARD-OBJECT of type POINT.
0. X: "unbound"
1. Y: "unbound"
2. Z: "unbound"
We got an object of type POINT
, but slots are unbound by
default: trying to access them will raise an UNBOUND-SLOT
condition:
(slot-value pt 'x) ;; => condition: the slot is unbound
slot-value
is setf
-able:
(setf (slot-value pt 'x) 1)
(slot-value pt 'x) ;; => 1
:initarg :foo
is the keyword we can pass tomake-instance
to give a value to this slot:
(make-instance 'person :name "me")
(again: slots are unbound by default)
:initform <val>
is the default value in case we didn't specify an initarg. This form is evaluated each time it's needed, in the lexical environment of thedefclass
.
Sometimes we see the following trick to clearly require a slot:
(defclass foo ()
((a
:initarg :a
:initform (error "you didn't supply an initial value for slot a"))))
;; #<STANDARD-CLASS FOO>
(make-instance 'foo) ;; => enters the debugger.
:accessor foo
: an accessor is both a getter and a setter. Its argument is a name that will become a generic function.
(name p1) ;; => "me"
(type-of #'name)
STANDARD-GENERIC-FUNCTION
:reader
and:writer
do what you expect. Only the:writer
issetf
-able.
If you don't specify any of these, you can still use slot-value
.
You can give a slot more than one :accessor
, :reader
or :initarg
.
We introduce two macros to make the access to slots shorter in some situations:
1- with-slots
allows to abbreviate several calls to slot-value. The
first argument is a list of slot names. The second argument evaluates
to a CLOS instance. This is followed by optional declarations and an
implicit progn
. Lexically during the evaluation of the body, an
access to any of these names as a variable is equivalent to accessing
the corresponding slot of the instance with slot-value
.
(with-slots (name lisper)
c1
(format t "got ~a, ~a~&" name lisper))
or
(with-slots ((n name)
(l lisper))
c1
(format t "got ~a, ~a~&" n l))
2- with-accessors
is equivalent, but instead of a list of slots it
takes a list of accessor functions. Any reference to the variable
inside the macro is equivalent to a call to the accessor function.
(with-accessors ((name name)
^^variable ^^accessor
(lisper lisper))
p1
(format t "name: ~a, lisper: ~a" name lisper))
:allocation
specifies whether this slot is local or shared.
-
a slot is local by default, that means it can be different for each instance of the class. In that case
:allocation
equals:instance
. -
a shared slot will always be equal for all instances of the class. We set it with
:allocation :class
.
In the following example, note how changing the value of the class
slot species
of p2
affects all instances of the
class (whether or not those instances exist yet).
(defclass person ()
((name :initarg :name :accessor name)
(species
:initform 'homo-sapiens
:accessor species
:allocation :class)))
;; Note that the slot "lisper" was removed in existing instances.
(inspect p1)
;; The object is a STANDARD-OBJECT of type PERSON.
;; 0. NAME: "me"
;; 1. SPECIES: HOMO-SAPIENS
;; > q
(defvar p2 (make-instance 'person))
(species p1)
(species p2)
;; HOMO-SAPIENS
(setf (species p2) 'homo-numericus)
;; HOMO-NUMERICUS
(species p1)
;; HOMO-NUMERICUS
(species (make-instance 'person))
;; HOMO-NUMERICUS
(let ((temp (make-instance 'person)))
(setf (species temp) 'homo-lisper))
;; HOMO-LISPER
(species (make-instance 'person))
;; HOMO-LISPER
Each slot accepts one :documentation
option. To obtain its documentation via documentation
, you need to obtain the slot object. This can be done compatibly using a library such as closer-mop. For instance:
(closer-mop:class-direct-slots (find-class 'my-class))
;; => list of slots (objects)
(find 'my-slot * :key #'closer-mop:slot-definition-name)
;; => find desired slot by name
(documentation * t) ; obtain its documentation
Note however that generally it may be better to document slot accessors instead, as a popular viewpoint is that slots are implementation details and not part of the public interface.
The :type
slot option may not do the job you expect it does. If you
are new to the CLOS, we suggest you skip this section and use your own
constructors to manually check slot types.
Indeed, whether slot types are being checked or not is undefined. See the Hyperspec.
Few implementations will do it. Clozure CL does it, SBCL does it since
its version 1.5.9 (November, 2019) or when safety is high ((declaim (optimise safety))
).
To do it otherwise, see this Stack-Overflow answer, and see also quid-pro-quo, a contract programming library.
(find-class 'point)
;; #<STANDARD-CLASS POINT 275B78DC>
(class-name (find-class 'point))
;; POINT
(class-of my-point)
;; #<STANDARD-CLASS POINT 275B78DC>
(typep my-point (class-of my-point))
;; T
CLOS classes are also instances of a CLOS class, and we can find out what that class is, as in the example below:
(class-of (class-of my-point))
;; #<STANDARD-CLASS STANDARD-CLASS 20306534>
Note: this is your first introduction to the MOP. You don't need that to get started !
The object my-point
is an instance of the class named point
, and the
class named point
is itself an instance of the class named
standard-class
. We say that the class named standard-class
is
the metaclass (i.e. the class of the class) of
my-point
. We can make good uses of metaclasses, as we'll see later.
As illustrated above, child
is a subclass of person
.
All objects inherit from the class standard-object
and t
.
Every child instance is also an instance of person
.
(type-of c1)
;; CHILD
(subtypep (type-of c1) 'person)
;; T
(ql:quickload "closer-mop")
;; ...
(closer-mop:subclassp (class-of c1) 'person)
;; T
The closer-mop library is the portable way to do CLOS/MOP operations.
A subclass inherits all of its parents' slots, and it can override any of their slot options. Common Lisp makes this process dynamic, great for REPL session, and we can even control parts of it (like, do something when a given slot is removed/updated/added, etc).
The class precedence list of a child
is thus:
child <- person <-- standard-object <- t
Which we can get with:
(closer-mop:class-precedence-list (class-of c1))
;; (#<standard-class child>
;; #<standard-class person>
;; #<standard-class standard-object>
;; #<sb-pcl::slot-class sb-pcl::slot-object>
;; #<sb-pcl:system-class t>)
However, the direct superclass of a child
is only:
(closer-mop:class-direct-superclasses (class-of c1))
;; (#<standard-class person>)
We can further inspect our classes with
class-direct-[subclasses, slots, default-initargs]
and many more functions.
How slots are combined follows some rules:
-
:accessor
and:reader
are combined by the union of accessors and readers from all the inherited slots. -
:initarg
: the union of initialization arguments from all the inherited slots. -
:initform
: we get the most specific default initial value form, i.e. the first:initform
for that slot in the precedence list. -
:allocation
is not inherited. It is controlled solely by the class being defined and defaults to:instance
.
Last but not least, be warned that inheritance is fairly easy to
misuse, and multiple inheritance is multiply so, so please take a
little care. Ask yourself whether foo
really wants to inherit from
bar
, or whether instances of foo
want a slot containing a bar
. A
good general guide is that if foo
and bar
are "same sort of thing"
then it's correct to mix them together by inheritance, but if they're
really separate concepts then you should use slots to keep them apart.
CLOS supports multiple inheritance.
(defclass baby (child person)
())
The first class on the list of parent classes is the most specific
one, child
's slots will take precedence over the person
's. Note
that both child
and person
have to be defined prior to defining
baby
in this example.
This section briefly covers two topics:
- redefinition of an existing class, which you might already have done by following our code snippets, and what we do naturally during development, and
- changing an instance of one class into an instance of another, a powerful feature of CLOS that you'll probably won't use very often.
We'll gloss over the details. Suffice it to say that everything's configurable by implementing methods exposed by the MOP.
To redefine a class, simply evaluate a new defclass
form. This then
takes the place of the old definition, the existing class object is
updated, and all instances of the class (and, recursively, its
subclasses) are lazily updated to reflect the new definition. You don't
have to recompile anything other than the new defclass
, nor to
invalidate any of your objects. Think about it for a second: this is awesome !
For example, with our person
class:
(defclass person ()
((name
:initarg :name
:accessor name)
(lisper
:initform nil
:accessor lisper)))
(setf p1 (make-instance 'person :name "me" ))
Changing, adding, removing slots,...
(lisper p1)
;; NIL
(defclass person ()
((name
:initarg :name
:accessor name)
(lisper
:initform t ;; <-- from nil to t
:accessor lisper)))
(lisper p1)
;; NIL (of course!)
(lisper (make-instance 'person :name "You"))
;; T
(defclass person ()
((name
:initarg :name
:accessor name)
(lisper
:initform nil
:accessor lisper)
(age ;; <-- new slot
:initarg :arg
:initform 18 ;; <-- default value
:accessor age)))
(age p1)
;; => 18. Correct. This is the default initform for this new slot.
(slot-value p1 'bwarf)
;; => "the slot bwarf is missing from the object #<person…>"
(setf (age p1) 30)
(age p1) ;; => 30
(defclass person ()
((name
:initarg :name
:accessor name)))
(slot-value p1 'lisper) ;; => slot lisper is missing.
(lisper p1) ;; => there is no applicable method for the generic function lisper when called with arguments #(lisper).
To change the class of an instance, use change-class
:
(change-class p1 'child)
;; we can also set slots of the new class:
(change-class p1 'child :can-walk-p nil)
(class-of p1)
;; #<STANDARD-CLASS CHILD>
(can-walk-p p1)
;; T
In the above example, I became a child
, and I inherited the can-walk-p
slot, which is true by default.
Every time we printed an object so far we got an output like
#<PERSON {1006234593}>
which doesn't say much.
What if we want to show more information ? Something like
#<PERSON me lisper: t>
Pretty printing is done by specializing the generic print-object
method for this class:
(defmethod print-object ((obj person) stream)
(print-unreadable-object (obj stream :type t)
(with-accessors ((name name)
(lisper lisper))
obj
(format stream "~a, lisper: ~a" name lisper))))
It gives:
p1
;; #<PERSON me, lisper: T>
print-unreadable-object
prints the #<...>
, that says to the reader
that this object can not be read back in. Its :type t
argument asks
to print the object-type prefix, that is, PERSON
. Without it, we get
#<me, lisper: T>
.
We used the with-accessors
macro, but of course for simple cases this is enough:
(defmethod print-object ((obj person) stream)
(print-unreadable-object (obj stream :type t)
(format stream "~a, lisper: ~a" (name obj) (lisper obj))))
Caution: trying to access a slot that is not bound by default will
lead to an error. Use slot-boundp
.
For reference, the following reproduces the default behaviour:
(defmethod print-object ((obj person) stream)
(print-unreadable-object (obj stream :type t :identity t)))
Here, :identity
to t
prints the {1006234593}
address.
Where we approach that we don't need CLOS objects to use CLOS.
Generously, the functions introduced in the last section also work on lisp objects which are not CLOS instances:
(find-class 'symbol)
;; #<BUILT-IN-CLASS SYMBOL>
(class-name *)
;; SYMBOL
(eq ** (class-of 'symbol))
;; T
(class-of ***)
;; #<STANDARD-CLASS BUILT-IN-CLASS>
We see here that symbols are instances of the system class
symbol
. This is one of 75 cases in which the language requires a
class to exist with the same name as the corresponding lisp
type. Many of these cases are concerned with CLOS itself (for
example, the correspondence between the type standard-class
and
the CLOS class of that name) or with the condition system (which
might or might not be built using CLOS classes in any given
implementation). However, 33 correspondences remain relating to
"traditional" lisp types:
array | hash-table | readtable |
bit-vector | integer | real |
broadcast-stream | list | sequence |
character | logical-pathname | stream |
complex | null | string |
concatenated-stream | number | string-stream |
cons | package | symbol |
echo-stream | pathname | synonym-stream |
file-stream | random-state | t |
float | ratio | two-way-stream |
function | rational | vector |
Note that not all "traditional" lisp types are included in this
list. (Consider: atom
, fixnum
, short-float
, and any type not
denoted by a symbol.)
The presence of t
is interesting. Just as every lisp
object is of type t
, every lisp object is also a member
of the class named t
. This is a simple example of
membership of more then one class at a time, and it brings into
question the issue of inheritance, which we will consider
in some detail later.
(find-class t)
;; #<BUILT-IN-CLASS T 20305AEC>
In addition to classes corresponding to lisp types, there is also a CLOS class for every structure type you define:
(defstruct foo)
FOO
(class-of (make-foo))
;; #<STRUCTURE-CLASS FOO 21DE8714>
The metaclass of a structure-object
is the class structure-class
. It is implementation-dependent whether
the metaclass of a "traditional" lisp object is standard-class
, structure-class
, or built-in-class
.
Restrictions:
built-in-class
: May not use make-instance
, may not use slot-value
, may not use defclass
to modify, may not create subclasses.
structure-class
: May not use make-instance
, might work with slot-value
(implementation-dependent). Use defstruct
to subclass application structure types. Consequences of modifying an existing structure-class
are undefined: full recompilation may be necessary.
standard-class
: None of these restrictions.
We already saw some introspection functions.
Your best option is to discover the closer-mop library and to keep the CLOS & MOP specifications at hand.
More functions:
closer-mop:class-default-initargs
closer-mop:class-direct-default-initargs
closer-mop:class-direct-slots
closer-mop:class-direct-subclasses
closer-mop:class-direct-superclasses
closer-mop:class-precedence-list
closer-mop:class-slots
closer-mop:classp
closer-mop:extract-lambda-list
closer-mop:extract-specializer-names
closer-mop:generic-function-argument-precedence-order
closer-mop:generic-function-declarations
closer-mop:generic-function-lambda-list
closer-mop:generic-function-method-class
closer-mop:generic-function-method-combination
closer-mop:generic-function-methods
closer-mop:generic-function-name
closer-mop:method-combination
closer-mop:method-function
closer-mop:method-generic-function
closer-mop:method-lambda-list
closer-mop:method-specializers
closer-mop:slot-definition
closer-mop:slot-definition-allocation
closer-mop:slot-definition-initargs
closer-mop:slot-definition-initform
closer-mop:slot-definition-initfunction
closer-mop:slot-definition-location
closer-mop:slot-definition-name
closer-mop:slot-definition-readers
closer-mop:slot-definition-type
closer-mop:slot-definition-writers
closer-mop:specializer-direct-generic-functions
closer-mop:specializer-direct-methods
closer-mop:standard-accessor-method
The command M-x slime-export-class will add the class symbols to the ":export" clause of your package definition. This way, you can export dozens of symbols all at once.
Imagine you have this class:
(defclass test ()
((foo :accessor foo)
(bar :reader bar)))
Using "M-x slime-export-class RET test RET" will export "test", "foot" and "bar".
Removing a slot from the class definition will alas not remove it from the export clause.
This works also on structures (only on SBCL and Clozure CL).
The library defclass/std
provides a macro to write shorter defclass
forms.
By default, it adds an accessor, an initarg and an initform to nil
to your slots definition:
This:
(defclass/std example ()
((slot1 slot2 slot3)))
expands to:
(defclass example ()
((slot1
:accessor slot1
:initarg :slot1
:initform nil)
(slot2
:accessor slot2
:initarg :slot2
:initform nil)
(slot3
:accessor slot3
:initarg :slot3
:initform nil)))
It does much more and it is very flexible, however it is seldom used by the Common Lisp community: use at your own risk©.
Recalling our person
and child
classes from the beginning:
(defclass person ()
((name
:initarg :name
:accessor name)))
;; => #<STANDARD-CLASS PERSON>
(defclass child (person)
())
;; #<STANDARD-CLASS CHILD>
(setf p1 (make-instance 'person :name "me"))
(setf c1 (make-instance 'child :name "Alice"))
Below we create methods, we specialize them, we use method combination (before, after, around), and qualifiers.
(defmethod greet (obj)
(format t "Are you a person ? You are a ~a.~&" (type-of obj)))
;; style-warning: Implicitly creating new generic function common-lisp-user::greet.
;; #<STANDARD-METHOD GREET (t) {1008EE4603}>
(greet :anything)
;; Are you a person ? You are a KEYWORD.
;; NIL
(greet p1)
;; Are you a person ? You are a PERSON.
(defgeneric greet (obj)
(:documentation "say hello"))
;; STYLE-WARNING: redefining COMMON-LISP-USER::GREET in DEFGENERIC
;; #<STANDARD-GENERIC-FUNCTION GREET (2)>
(defmethod greet ((obj person))
(format t "Hello ~a !~&" (name obj)))
;; #<STANDARD-METHOD GREET (PERSON) {1007C26743}>
(greet p1) ;; => "Hello me !"
(greet c1) ;; => "Hello Alice !"
(defmethod greet ((obj child))
(format t "ur so cute~&"))
;; #<STANDARD-METHOD GREET (CHILD) {1008F3C1C3}>
(greet p1) ;; => "Hello me !"
(greet c1) ;; => "ur so cute"
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Method combination: before, after, around.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defmethod greet :before ((obj person))
(format t "-- before person~&"))
#<STANDARD-METHOD GREET :BEFORE (PERSON) {100C94A013}>
(greet p1)
;; -- before person
;; Hello me
(defmethod greet :before ((obj child))
(format t "-- before child~&"))
;; #<STANDARD-METHOD GREET :BEFORE (CHILD) {100AD32A43}>
(greet c1)
;; -- before child
;; -- before person
;; ur so cute
(defmethod greet :after ((obj person))
(format t "-- after person~&"))
;; #<STANDARD-METHOD GREET :AFTER (PERSON) {100CA2E1A3}>
(greet p1)
;; -- before person
;; Hello me
;; -- after person
(defmethod greet :after ((obj child))
(format t "-- after child~&"))
;; #<STANDARD-METHOD GREET :AFTER (CHILD) {10075B71F3}>
(greet c1)
;; -- before child
;; -- before person
;; ur so cute
;; -- after person
;; -- after child
(defmethod greet :around ((obj child))
(format t "Hello my dear~&"))
;; #<STANDARD-METHOD GREET :AROUND (CHILD) {10076658E3}>
(greet c1) ;; Hello my dear
;; call-next-method
(defmethod greet :around ((obj child))
(format t "Hello my dear~&")
(when (next-method-p)
(call-next-method)))
;; #<standard-method greet :around (child) {100AF76863}>
(greet c1)
;; Hello my dear
;; -- before child
;; -- before person
;; ur so cute
;; -- after person
;; -- after child
;;;;;;;;;;;;;;;;;
;; Adding in &key
;;;;;;;;;;;;;;;;;
;; In order to add "&key" to our generic method, we need to remove its definition first.
(fmakunbound 'greet) ;; with Slime: C-c C-u (slime-undefine-function)
(defmethod greet ((obj person) &key talkative)
(format t "Hello ~a~&" (name obj))
(when talkative
(format t "blah")))
(defgeneric greet (obj &key &allow-other-keys)
(:documentation "say hi"))
(defmethod greet (obj &key &allow-other-keys)
(format t "Are you a person ? You are a ~a.~&" (type-of obj)))
(defmethod greet ((obj person) &key talkative &allow-other-keys)
(format t "Hello ~a !~&" (name obj))
(when talkative
(format t "blah")))
(greet p1 :talkative t) ;; ok
(greet p1 :foo t) ;; still ok
;;;;;;;;;;;;;;;;;;;;;;;
(defgeneric greet (obj)
(:documentation "say hello")
(:method (obj)
(format t "Are you a person ? You are a ~a~&." (type-of obj)))
(:method ((obj person))
(format t "Hello ~a !~&" (name obj)))
(:method ((obj child))
(format t "ur so cute~&")))
;;;;;;;;;;;;;;;;
;;; Specializers
;;;;;;;;;;;;;;;;
(defgeneric feed (obj meal-type)
(:method (obj meal-type)
(declare (ignorable meal-type))
(format t "eating~&")))
(defmethod feed (obj (meal-type (eql :dessert)))
(declare (ignorable meal-type))
(format t "mmh, dessert !~&"))
(feed c1 :dessert)
;; mmh, dessert !
(defmethod feed ((obj child) (meal-type (eql :soup)))
(declare (ignorable meal-type))
(format t "bwark~&"))
(feed p1 :soup)
;; eating
(feed c1 :soup)
;; bwark
A generic function
is a lisp function which is associated
with a set of methods and dispatches them when it's invoked. All
the methods with the same function name belong to the same generic
function.
The defmethod
form is similar to a defun
. It associates a body of
code with a function name, but that body may only be executed if the
types of the arguments match the pattern declared by the lambda list.
They can have optional, keyword and &rest
arguments.
The defgeneric
form defines the generic function. If we write a
defmethod
without a corresponding defgeneric
, a generic function
is automatically created (see examples).
It is generally a good idea to write the defgeneric
s. We can add a
default implementation and even some documentation.
(defgeneric greet (obj)
(:documentation "says hi")
(:method (obj)
(format t "Hi")))
The required parameters in the method's lambda list may take one of the following three forms:
1- a simple variable:
(defmethod greet (foo)
...)
This method can take any argument, it is always applicable.
The variable foo
is bound to the corresponding argument value, as
usual.
2- a variable and a specializer, as in:
(defmethod greet ((foo person))
...)
In this case, the variable foo
is bound to the corresponding
argument only if that argument is of specializer class person
or a subclass,
like child
(indeed, a "child" is also a "person").
If any argument fails to match its specializer then the method is not applicable and it cannot be executed with those arguments.We'll get an error message like "there is no applicable method for the generic function xxx when called with arguments yyy".
Only required parameters can be specialized. We can't specialize on optional &key
arguments.
3- a variable and an eql specializer
(defmethod feed ((obj child) (meal-type (eql :soup)))
(declare (ignorable meal-type))
(format t "bwark~&"))
(feed c1 :soup)
;; "bwark"
In place of a simple symbol (:soup
), the eql specializer can be any
lisp form. It is evaluated at the same time of the defmethod.
You can define any number of methods with the same function name but
with different specializers, as long as the form of the lambda list is
congruent with the shape of the generic function. The system chooses
the most specific applicable method and executes its body. The most
specific method is the one whose specializers are nearest to the head
of the class-precedence-list
of the argument (classes on the left of
the lambda list are more specific). A method with specializers is more
specific to one without any.
Notes:
-
It is an error to define a method with the same function name as an ordinary function. If you really want to do that, use the shadowing mechanism.
-
To add or remove
keys
orrest
arguments to an existing generic method's lambda list, you will need to delete its declaration withfmakunbound
(orC-c C-u
(slime-undefine-function) with the cursor on the function in Slime) and start again. Otherwise, you'll see:
attempt to add the method
#<STANDARD-METHOD NIL (#<STANDARD-CLASS CHILD>) {1009504233}>
to the generic function
#<STANDARD-GENERIC-FUNCTION GREET (2)>;
but the method and generic function differ in whether they accept
&REST or &KEY arguments.
-
Methods can be redefined (exactly as for ordinary functions).
-
The order in which methods are defined is irrelevant, although any classes on which they specialize must already exist.
-
An unspecialized argument is more or less equivalent to being specialized on the class
t
. The only difference is that all specialized arguments are implicitly taken to be "referred to" (in the sense ofdeclare ignore
.) -
Each
defmethod
form generates (and returns) a CLOS instance, of classstandard-method
. -
An
eql
specializer won't work as is with strings. Indeed, strings needequal
orequalp
to be compared. But, we can assign our string to a variable and use the variable both in theeql
specializer and for the function call. -
All the methods with the same function name belong to the same generic function.
-
All slot accessors and readers defined by
defclass
are methods. They can override or be overridden by other methods on the same generic function.
See more about defmethod on the CLHS.
Multimethods explicitly specialize more than one of the generic function's required parameters.
They don't belong to a particular class. Meaning, we don't have to decide on the class that would be best to host this method, as we might have to in other languages.
(defgeneric hug (a b)
(:documentation "Hug between two persons."))
;; #<STANDARD-GENERIC-FUNCTION HUG (0)>
(defmethod hug ((a person) (b person))
:person-person-hug)
(defmethod hug ((a person) (b child))
:person-child-hug)
Read more on Practical Common Lisp.
In Lisp, we can define setf
counterparts of functions or methods. We
might want this to have more control on how to update an object.
(defmethod (setf name) (new-val (obj person))
(if (equalp new-val "james bond")
(format t "Dude that's not possible.~&")
(setf (slot-value obj 'name) new-val)))
(setf (name p1) "james bond") ;; -> no rename
If you know Python, this behaviour is provided by the @property
decorator.
When a generic function is invoked, the application cannot directly invoke a method. The dispatch mechanism proceeds as follows:
- compute the list of applicable methods
- if no method is applicable then signal an error
- sort the applicable methods in order of specificity
- invoke the most specific method.
Our greet
generic function has three applicable methods:
(closer-mop:generic-function-methods #'greet)
(#<STANDARD-METHOD GREET (CHILD) {10098406A3}>
#<STANDARD-METHOD GREET (PERSON) {1009008EC3}>
#<STANDARD-METHOD GREET (T) {1008E6EBB3}>)
During the execution of a method, the remaining applicable methods
are still accessible, via the local function
call-next-method
. This function has lexical scope within
the body of a method but indefinite extent. It invokes the next most
specific method, and returns whatever value that method returned. It
can be called with either:
-
no arguments, in which case the next method will receive exactly the same arguments as this method did, or
-
explicit arguments, in which case it is required that the sorted set of methods applicable to the new arguments must be the same as that computed when the generic function was first called.
For example:
(defmethod greet ((obj child))
(format t "ur so cute~&")
(when (next-method-p)
(call-next-method)))
;; STYLE-WARNING: REDEFINING GREET (#<STANDARD-CLASS CHILD>) in DEFMETHOD
;; #<STANDARD-METHOD GREET (child) {1003D3DB43}>
(greet c1)
;; ur so cute
;; Hello Alice !
Calling call-next-method
when there is no next method
signals an error. You can find out whether a next method exists by
calling the local function next-method-p
(which also has
has lexical scope and indefinite extent).
Note finally that the body of every method establishes a block with the same name as the method’s generic function. If you return-from
that name you are exiting the current method, not the call to the enclosing generic function.
In our "Diving in" examples, we saw some use of the :before
, :after
and :around
qualifiers:
(defmethod foo :before (obj) (...))
(defmethod foo :after (obj) (...))
(defmethod foo :around (obj) (...))
By default, in the standard method combination framework provided by CLOS, we can only use one of those three qualifiers, and the flow of control is as follows:
- a before-method is called, well, before the applicable primary method. If they are many before-methods, all are called. The most specific before-method is called first (child before person).
- the most specific applicable primary method (a method without qualifiers) is called (only one).
- all applicable after-methods are called. The most specific one is called last (after-method of person, then after-method of child).
The generic function returns the value of the primary method. Any values of the before or after methods are ignored. They are used for their side effects.
And then we have around-methods. They are wrappers around the core mechanism we just described. They can be useful to catch return values or to set up an environment around the primary method (set up a catch, a lock, timing an execution,…).
If the dispatch mechanism finds an around-method, it calls it and
returns its result. If the around-method has a call-next-method
, it
calls the next most applicable around-method. It is only when we reach
the primary method that we start calling the before and after-methods.
Thus, the full dispatch mechanism for generic functions is as follows:
-
compute the applicable methods, and partition them into separate lists according to their qualifier;
-
if there is no applicable primary method then signal an error;
-
sort each of the lists into order of specificity;
-
execute the most specific
:around
method and return whatever that returns; -
if an
:around
method invokescall-next-method
, execute the next most specific:around
method; -
if there were no
:around
methods in the first place, or if an:around
method invokescall-next-method
but there are no further:around
methods to call, then proceed as follows:a. run all the
:before
methods, in order, ignoring any return values and not permitting calls tocall-next-method
ornext-method-p
;b. execute the most specific primary method and return whatever that returns;
c. if a primary method invokes
call-next-method
, execute the next most specific primary method;d. if a primary method invokes
call-next-method
but there are no further primary methods to call then signal an error;e. after the primary method(s) have completed, run all the
:after
methods, in reverse order, ignoring any return values and not permitting calls tocall-next-method
ornext-method-p
.
Think of it as an onion, with all the :around
methods in the outermost layer, :before
and
:after
methods in the middle layer, and primary methods
on the inside.
The default method combination type we just saw is named standard
,
but other method combination types are available, and no need to say
that you can define your own.
The built-in types are:
progn + list nconc and max or append min
You notice that these types are named after a lisp operator. Indeed,
what they do is they define a framework that combines the applicable
primary methods inside a call to the lisp operator of that name. For
example, using the progn
combination type is equivalent to calling all
the primary methods one after the other:
(progn
(method-1 args)
(method-2 args)
(method-3 args))
Here, unlike the standard mechanism, all the primary methods applicable for a given object are called, the most specific first.
To change the combination type, we set the :method-combination
option of defgeneric
and we use it as the methods' qualifier:
(defgeneric foo (obj)
(:method-combination progn))
(defmethod foo progn ((obj obj))
(...))
An example with progn:
(defgeneric dishes (obj)
(:method-combination progn)
(:method progn (obj)
(format t "- clean and dry.~&"))
(:method progn ((obj person))
(format t "- bring a person's dishes~&"))
(:method progn ((obj child))
(format t "- bring the baby dishes~&")))
;; #<STANDARD-GENERIC-FUNCTION DISHES (3)>
(dishes c1)
;; - bring the baby dishes
;; - bring a person's dishes
;; - clean and dry.
(greet c1)
;; ur so cute --> only the most applicable method was called.
Similarly, using the list
type is equivalent to returning the list
of the values of the methods.
(list
(method-1 args)
(method-2 args)
(method-3 args))
(defgeneric tidy (obj)
(:method-combination list)
(:method list (obj)
:foo)
(:method list ((obj person))
:books)
(:method list ((obj child))
:toys))
;; #<STANDARD-GENERIC-FUNCTION TIDY (3)>
(tidy c1)
;; (:toys :books :foo)
Around methods are accepted:
(defmethod tidy :around (obj)
(let ((res (call-next-method)))
(format t "I'm going to clean up ~a~&" res)
(when (> (length res)
1)
(format t "that's too much !~&"))))
(tidy c1)
;; I'm going to clean up (toys book foo)
;; that's too much !
Note that these operators don't support before
, after
and around
methods (indeed, there is no room for them anymore). They do support
around methods, where call-next-method
is allowed, but they don't
support calling call-next-method
in the primary methods (it would
indeed be redundant since all primary methods are called, or clunky to
not call one).
CLOS allows us to define a new operator as a method combination type, be it a lisp function, macro or special form. We'll let you refer to the books if you feel the need.
It is possible to trace the method combination, but this is implementation dependent.
In SBCL, we can use (trace foo :methods t)
. See this post by an SBCL core developer.
For example, given a generic:
(defgeneric foo (x)
(:method (x) 3))
(defmethod foo :around ((x fixnum))
(1+ (call-next-method)))
(defmethod foo ((x integer))
(* 2 (call-next-method)))
(defmethod foo ((x float))
(* 3 (call-next-method)))
(defmethod foo :before ((x single-float))
'single)
(defmethod foo :after ((x double-float))
'double)
Let's trace it:
(trace foo :methods t)
(foo 2.0d0)
0: (FOO 2.0d0)
1: ((SB-PCL::COMBINED-METHOD FOO) 2.0d0)
2: ((METHOD FOO (FLOAT)) 2.0d0)
3: ((METHOD FOO (T)) 2.0d0)
3: (METHOD FOO (T)) returned 3
2: (METHOD FOO (FLOAT)) returned 9
2: ((METHOD FOO :AFTER (DOUBLE-FLOAT)) 2.0d0)
2: (METHOD FOO :AFTER (DOUBLE-FLOAT)) returned DOUBLE
1: (SB-PCL::COMBINED-METHOD FOO) returned 9
0: FOO returned 9
9
There is a difference between declaring methods inside a defgeneric
body or by writing multiple defmethod
s: the two methods handle
re-definition of methods differently. defgeneric
will delete methods
that are not in its body anymore.
Below we define a new generic function, using two defmethod
that
specialize on person
and child
:
(defmethod goodbye ((p person))
(format t "goodbye ~a.~&" (name p)))
(defmethod goodbye ((c child))
(format t "love you lil' one <3~&"))
You can try them with (goodbye (make-instance 'person :name "you"))
.
Now, later in your work session, you decide that you don't need the
one specializing on child
any more. You delete its source code. But
the method still exists in the image. You have to programmatically
remove the method, see below.
Had you used defgeneric
, all the methods would have been updated,
added or deleted. We have defined the tidy
generic function already
with three methods:
(defgeneric tidy (obj)
(:method-combination list)
(:method list (obj)
:foo)
(:method list ((obj person))
:books)
(:method list ((obj child))
:toys))
It works for any object type, a person or a child. Try it on a string:
(tidy "tidy what?")
, it works.
Now remove this declaration from the defgeneric
:
(defgeneric tidy (obj)
(:method-combination list)
;;(:method list (obj) ;; <--- commented out
;; :foo)
(:method list ((obj person))
:books)
(:method list ((obj child))
:toys))
Try to call it again: you get a "no applicable method" error:
There is no applicable method for the generic function
#<STANDARD-GENERIC-FUNCTION TRADESIGNAL::TIDY (2)>
when called with arguments
("tidy what?").
This might or might not be important to you during development, but knowing this can help you keep your lisp image in sync with your source code. Otherwise, you can remove an old method when it gets on your way.
First, we need to find the method object:
(find-method #'goodbye nil (list (find-class 'child)))
;; => #<STANDARD-METHOD GOODBYE (CHILD) {10073EFD73}>
find-method
takes as arguments: a function reference, a qualifier
(like before, after or around), and a list of class specializers.
Once you found the method, use remove-method
.
You could use (fmakunbound 'goodbye)
, but this makes all methods
unbound.
We gather here some examples that make use of the framework provided by the meta-object protocol, the configurable object system that rules Lisp's object system. We touch advanced concepts so, new reader, don't worry: you don't need to understand this section to start using the Common Lisp Object System.
We won't explain much about the MOP here, but hopefully sufficiently to make you see its possibilities or to help you understand how some CL libraries are built. We invite you to read the books referenced in the introduction.
Metaclasses are needed to control the behaviour of other classes.
As announced, we won't talk much. See also Wikipedia for metaclasses or CLOS.
The standard metaclass is standard-class
:
(class-of p1) ;; #<STANDARD-CLASS PERSON>
But we'll change it to one of our own, so that we'll be able to count the creation of instances. This same mechanism could be used to auto increment the primary key of a database system (this is how the Postmodern or Mito libraries do), to log the creation of objects, etc.
Our metaclass inherits from standard-class
:
(defclass counted-class (standard-class)
((counter :initform 0)))
#<STANDARD-CLASS COUNTED-CLASS>
(unintern 'person)
;; this is necessary to change the metaclass of person.
;; or (setf (find-class 'person) nil)
;; https://stackoverflow.com/questions/38811931/how-to-change-classs-metaclass#38812140
(defclass person ()
((name
:initarg :name
:accessor name))
(:metaclass counted-class)) ;; <- metaclass
;; #<COUNTED-CLASS PERSON>
;; ^^^ not standard-class anymore.
The :metaclass
class option can appear only once.
Actually you should have gotten a message asking to implement
validate-superclass
. So, still with the closer-mop
library:
(defmethod closer-mop:validate-superclass ((class counted-class)
(superclass standard-class))
t)
Now we can control the creation of new person
instances:
(defmethod make-instance :after ((class counted-class) &key)
(incf (slot-value class 'counter)))
;; #<STANDARD-METHOD MAKE-INSTANCE :AFTER (COUNTED-CLASS) {1007718473}>
See that an :after
qualifier is the safest choice, we let the
standard method run as usual and return a new instance.
The &key
is necessary, remember that make-instance
is given initargs.
Now testing:
(defvar p3 (make-instance 'person :name "adam"))
#<PERSON {1007A8F5B3}>
(slot-value p3 'counter)
;; => error. No, our new slot isn't on the person class.
(slot-value (find-class 'person) 'counter)
;; 1
(make-instance 'person :name "eve")
;; #<PERSON {1007AD5773}>
(slot-value (find-class 'person) 'counter)
;; 2
It's working.
To further control the creation of object instances, we can specialize the method
initialize-instance
. It is called by make-instance
, just after
a new instance was created but wasn't initialized yet with the
default initargs and initforms.
It is recommended (Keene) to create an after method, since creating a primary method would prevent slots' initialization.
(defmethod initialize-instance :after ((obj person) &key)
;; note the &key in the arglist: ^^^^
(do something with obj))
A typical example would be to validate the initial values. Here we'll check that the person's name is longer than 3 characters:
(defmethod initialize-instance :after ((obj person) &key)
(with-slots (name) obj
(assert (>= (length name) 3))))
So this call doesn't work anymore:
(make-instance 'person :name "me")
;; The assertion (>= #1=(LENGTH NAME) 3) failed with #1# = 2.
;; [Condition of type SIMPLE-ERROR]
We are prompted into the interactive debugger and we are given a choice of restarts (continue, retry, abort).
So while we're at it, here's an assertion that uses the debugger
features to offer to change "name". We give assert
a list of places
that can be changed from the debugger:
(defmethod INITIALIZE-INSTANCE :after ((obj person) &key)
(with-slots (name) obj
(assert (>= (length name) 3)
(name) ;; <-- list of places
"The value of name is ~a. It should be longer than 3 characters." name)))
We get:
The value of name is me. It should be longer than 3 characters.
[Condition of type SIMPLE-ERROR]
Restarts:
0: [CONTINUE] Retry assertion with new value for NAME.
^^^^^^^^^^^^ our new restart
1: [RETRY] Retry SLIME REPL evaluation request.
2: [*ABORT] Return to SLIME's top level.
Another rationale. The CLOS implementation of
make-instance
is in two stages: allocate the new object,
and then pass it along with all the make-instance
keyword
arguments, to the generic function
initialize-instance
. Implementors and application writers
define :after
methods on
initialize-instance
, to initialize the slots of the
instance. The system-supplied primary method does this with regard to
(a) :initform
and :initarg
values supplied
with the class was defined and (b) the keywords passed through from
make-instance
. Other methods can extend this behaviour as
they see fit. For example, they might accept an additional keyword
which invokes a database access to fill certain slots. The lambda list
for initialize-instance
is:
initialize-instance instance &rest initargs &key &allow-other-keys
Suppose you created a "circle" class, with coordinates and a
diameter. Later on, you decide to replace the diameter by a
radius. You want all the existing objects to be cleverly updated:
the radius should have the diameter value, divided by 2. Use
update-instance-for-redefined-class
.
Its parameters are:
- instance: the object instance that is being updated
- added-slots: a list of added slots
- discarded-slots: a list of discarded slots
- property-list: a plist that captured the slot names and values of all the discarded-slots with values in the original instance.
- initargs: an initialization argument list.
&key
catches them below.
and it returns an object.
We actually don't call the method direcly, but we use a :before
method:
(defmethod update-instance-for-redefined-class
:before ((obj circle) added deleted plist-values &key)
(format t "plist values: ~a~&" plist-values)
(let ((diameter (getf plist-values 'diameter)))
(setf (radius obj) (/ diameter 2))))
Here's how to try it. Start with a circle
class:
(defclass circle ()
((diameter :accessor diameter :initform 9)))
and create a circle object:
(make-instance 'circle)
inspect it or check its diameter value.
Now write and compile a new class definition:
(defclass circle ()
((radius :accessor radius)))
Nothing happens yet, you don't see the output of our "plist values" print.
Inspect or describe
the object: now it will be updated, and you'll
find the radius
slot.
Existing objects are updated lazily.
See more on the HyperSpec or on the Community Spec.
Now imagine you are working with the circle
class, but you realize
you only need a surface
kind of objects. You will discard the circle
class altogether, but you want your existing objects to be updated -to
this new class, and compute new slots intelligently. Use
update-instance-for-different-class
.
See more on the HyperSpec or on the Community Spec.
And see more in the books!