Slots is a Python library designed to allow the user to explore and use simple multi-armed bandit (MAB) strategies. The basic concept behind the multi-armed bandit problem is that you are faced with n choices (e.g. Slot machines, medicines, or UI/UX designs), each of which results in a 'win' with some unknown probability. This slot game I was playing on my phone was really simple. It had only 3 slots with different items in each. You had to push the Spin button in order to spin the slots, and you would win a small amount of coins if two or three slots were alike. Of course, 3 slots were better than 2.
Storybook - ygkn.github.io.
CLOS is the “Common Lisp Object System”, arguably one of the mostpowerful 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. Forexample, changing a class definition will update the existingobjects, 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 methoddefinitions are not tied together,
- it has excellent introspection capabilities,
- it is provided by a meta-object protocol, which provides astandard interface to the CLOS, and can be used to create new objectsystems.
The functionality belonging to this name was added to the Common Lisplanguage between the publication of Steele’s first edition of “CommonLisp, the Language” in 1984 and the formalization of the language asan ANSI standard ten years later.
This page aims to give a good understanding of how to use CLOS, butonly 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.
- and for reference, the complete CLOS-MOP specifications.
Classes and instances
Diving in
Let’s dive in with an example showing class definition, creation ofobjects, slot access, methods specialized for a given class, andinheritance.
Defining classes (defclass)
The macro used for defining new data types in CLOS is defclass
.
We used it like this:
This gives us a CLOS type (or class) called person
and two slots,named name
and lisper
.
The general form of defclass
is:
So, our person
class doesn’t explicitly inherit from another class(it gets the empty parentheses ()
). However it still inherits by default fromthe class t
and from standard-object
. See below under“inheritance”.
We could write a minimal class definition without slots options like this:
or even without slots specifiers: (defclass point () ())
.
Creating objects (make-instance)
We create instances of a class with make-instance
:
It is generally good practice to define a constructor:
This has the direct advantage that you can control the requiredarguments. You should now export the constructor from your package andnot the class itself.
Slots
A function that always works (slot-value)
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:
We got an object of type POINT
, but slots are unbound bydefault: trying to access them will raise an UNBOUND-SLOT
condition:
slot-value
is setf
-able:
Initial and default values (initarg, initform)
:initarg :foo
is the keyword we can pass tomake-instance
togive a value to this slot:
(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:
Getters and setters (accessor, reader, writer)
:accessor foo
: an accessor is both a getter and asetter. Its argument is a name that will become a genericfunction.
: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. Thefirst argument is a list of slot names. The second argument evaluatesto a CLOS instance. This is followed by optional declarations and animplicit progn
. Lexically during the evaluation of the body, anaccess to any of these names as a variable is equivalent to accessingthe corresponding slot of the instance with slot-value
.
or
2- with-accessors
is equivalent, but instead of a list of slots ittakes a list of accessor functions. Any reference to the variableinside the macro is equivalent to a call to the accessor function.
Class VS instance slots
: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 classslot species
of p2
affects all instances of theclass (whether or not those instances exist yet).
Slot documentation
Each slot accepts one :documentation
option.
Slot type
The :type
slot option may not do the job you expect it does. If youare new to the CLOS, we suggest you skip this section and use your ownconstructors 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 sinceits 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, class-name, class-of
CLOS classes are also instances of a CLOS class, and we can find outwhat that class is, as in the example below:
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 theclass named point
is itself an instance of the class namedstandard-class
. We say that the class named standard-class
isthe metaclass (i.e. the class of the class) ofmy-point
. We can make good uses of metaclasses, as we’ll see later.
Subclasses and inheritance
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
.
The closer-mop library is theportable way to do CLOS/MOP operations.
A subclass inherits all of its parents slots, and it can override anyof their slot options. Common Lisp makes this process dynamic, greatfor REPL session, and we can even control parts of it (like, dosomething when a given slot is removed/updated/added, etc).
The class precedence list of a child
is thus:
Which we can get with:
However, the direct superclass of a child
is only:
We can further inspect our classes withclass-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 theinherited slots.:initform
: we get the most specific default initial valueform, i.e. the first:initform
for that slot in the precedencelist.:allocation
is not inherited. It is controlled solely by the classbeing defined and defaults to:instance
.
Last but not least, be warned that inheritance is fairly easy tomisuse, and multiple inheritance is multiply so, so please take alittle care. Ask yourself whether foo
really wants to inherit frombar
, or whether instances of foo
want a slot containing a bar
. Agood 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’rereally separate concepts then you should use slots to keep them apart.
Multiple inheritance
CLOS supports multiple inheritance.
The first class on the list of parent classes is the most specificone, child
’s slots will take precedence over the person
’s. Notethat both child
and person
have to be defined prior to definingbaby
in this example.
Redefining and changing a class
This section briefly covers two topics:
- redefinition of an existing class, which you might already have doneby following our code snippets, and what we do naturally duringdevelopment, 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’sconfigurable by implementing methods exposed by the MOP.
To redefine a class, simply evaluate a new defclass
form. This thentakes the place of the old definition, the existing class object isupdated, and all instances of the class (and, recursively, itssubclasses) are lazily updated to reflect the new definition. You don’thave to recompile anything other than the new defclass
, nor toinvalidate any of your objects. Think about it for a second: this is awesome !
For example, with our person
class:
Changing, adding, removing slots,…
To change the class of an instance, use change-class
:
In the above example, I became a child
, and I inherited the can-walk-p
slot, which is true by default.
Pretty printing
Every time we printed an object so far we got an output like
which doesn’t say much.
What if we want to show more information ? Something like
Pretty printing is done by specializing the generic print-object
method for this class:
It gives:
print-unreadable-object
prints the #<...>
, that says to the readerthat this object can not be read back in. Its :type t
argument asksto 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:
Caution: trying to access a slot that is not bound by default willlead to an error. Use slot-boundp
.
For reference, the following reproduces the default behaviour:
Here, :identity
to t
prints the {1006234593}
address.
Classes of traditional lisp types
Where we approach that we don’t need CLOS objects to use CLOS.
Generously, the functions introduced in the last section also work onlisp objects which are not CLOS instances:
We see here that symbols are instances of the system classsymbol
. This is one of 75 cases in which the language requires aclass to exist with the same name as the corresponding lisptype. Many of these cases are concerned with CLOS itself (forexample, the correspondence between the type standard-class
andthe CLOS class of that name) or with the condition system (whichmight or might not be built using CLOS classes in any givenimplementation). 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 thislist. (Consider: atom
, fixnum
, short-float
, and any type notdenoted by a symbol.)
The presence of t
is interesting. Just as every lispobject is of type t
, every lisp object is also a memberof the class named t
. This is a simple example ofmembership of more then one class at a time, and it brings intoquestion the issue of inheritance, which we will considerin some detail later.
In addition to classes corresponding to lisp types, there is also a CLOS class for every structure type you define:
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. |
Introspection
we already saw some introspection functions.
Your best option is to discover thecloser-mop library and tokeep the CLOS & MOP specifications athand.
More functions:
See also
defclass/std: write shorter classes
The library defclass/stdprovides 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:
expands to:
It does much more and it is very flexible, however it is seldom usedby the Common Lisp community: use at your own risks©.
Methods
Diving in
Recalling our person
and child
classes from the beginning:
Below we create methods, we specialize them, we use method combination(before, after, around), and qualifiers.
Generic functions (defgeneric, defmethod)
A generic function
is a lisp function which is associatedwith a set of methods and dispatches them when it’s invoked. Allthe methods with the same function name belong to the same genericfunction.
The defmethod
form is similar to a defun
. It associates a body ofcode with a function name, but that body may only be executed if thetypes 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 adefmethod
without a corresponding defgeneric
, a generic functionis automatically created (see examples).
It is generally a good idea to write the defgeneric
s. We can add adefault implementation and even some documentation.
The required parameters in the method’s lambda list may take one ofthe following three forms:
1- a simple variable:
This method can take any argument, it is always applicable.
The variable foo
is bound to the corresponding argument value, asusual.
2- a variable and a specializer, as in:
In this case, the variable foo
is bound to the correspondingargument 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 itsspecializer then the method is not applicable and it cannot beexecuted with those arguments.We’ll get an error message like“there is no applicable method for the generic function xxx whencalled with arguments yyy”.
Only required parameters can be specialized. We can’t specialize on optional &key
arguments.
3- a variable and an eql specializer
In place of a simple symbol (:soup
), the eql specializer can be anylisp form. It is evaluated at the same time of the defmethod.
You can define any number of methods with the same function name butwith different specializers, as long as the form of the lambda list iscongruent with the shape of the generic function. The system choosesthe most specific applicable method and executes its body. The mostspecific method is the one whose specializers are nearest to the headof the class-precedence-list
of the argument (classes on the left ofthe lambda list are more specific). A method with specializers is morespecific to one without any.
Notes:
It is an error to define a method with the same function name asan ordinary function. If you really want to do that, use theshadowing mechanism.
To add or remove
keys
orrest
arguments to an existing genericmethod’s lambda list, you will need to delete its declaration withfmakunbound
(orC-c C-u
(slime-undefine-function) with thecursor on the function in Slime) and start again. Otherwise,you’ll see:
Methods can be redefined (exactly as for ordinary functions).
The order in which methods are defined is irrelevant, althoughany classes on which they specialize must already exist.
An unspecialized argument is more or less equivalent to beingspecialized on the class
t
. The only difference is thatall specialized arguments are implicitly taken to be “referred to” (inthe sense ofdeclare ignore
.)Each
defmethod
form generates (and returns) a CLOSinstance, of classstandard-method
.An
eql
specializer won’t work as is with strings. Indeed, stringsneedequal
orequalp
to be compared. But, we can assign our stringto a variable and use the variable both in theeql
specializer andfor 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
Multimethods explicitly specialize more than one of the genericfunction’s required parameters.
They don’t belong to a particular class. Meaning, we don’t have todecide on the class that would be best to host this method, as we mighthave to in other languages.
Read more on Practical Common Lisp.
Controlling setters (setf-ing methods)
In Lisp, we can define setf
counterparts of functions or methods. Wemight want this to have more control on how to update an object.
If you know Python, this behaviour is provided by the @property
decorator.
Dispatch mechanism and next methods
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:
During the execution of a method, the remaining applicable methodsare still accessible, via the local functioncall-next-method
. This function has lexical scope withinthe body of a method but indefinite extent. It invokes the next mostspecific method, and returns whatever value that method returned. Itcan be called with either:
no arguments, in which case the next method willreceive exactly the same arguments as this method did, or
explicit arguments, in which case it is required that thesorted set of methods applicable to the new arguments must be the sameas that computed when the generic function was first called.
For example:
Calling call-next-method
when there is no next methodsignals an error. You can find out whether a next method exists bycalling the local function next-method-p
(which also hashas 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.
Method qualifiers (before, after, around)
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 byCLOS, 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 primarymethod. If they are many before-methods, all are called. Themost specific before-method is called first (child before person).
- the most specific applicable primary method (a method withoutqualifiers) is called (only one).
- all applicable after-methods are called. The most specific one iscalled last (after-method of person, then after-method of child).
The generic function returns the value of the primary method. Anyvalues of the before or after methods are ignored. They are used fortheir side effects.
And then we have around-methods. They are wrappers around the coremechanism we just described. They can be useful to catch return valuesor 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 andreturns its result. If the around-method has a call-next-method
, itcalls the next most applicable around-method. It is only when we reachthe 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 intoseparate lists according to their qualifier;
- if there is no applicable primary method then signal anerror;
- sort each of the lists into order of specificity;
- execute the most specific
:around
method andreturn 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 firstplace, 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.
Other method combinations
The default method combination type we just saw is named standard
,but other method combination types are available, and no need to saythat you can define your own.
The built-in types are:
You notice that these types are named after a lisp operator. Indeed,what they do is they define a framework that combines the applicableprimary methods inside a call to the lisp operator of that name. Forexample, using the progn
combination type is equivalent to calling allthe primary methods one after the other:
Here, unlike the standard mechanism, all the primary methodsapplicable for a given object are called, the most specificfirst.
To change the combination type, we set the :method-combination
option of defgeneric
and we use it as the methods’ qualifier:
An example with progn:
Similarly, using the list
type is equivalent to returning the listof the values of the methods.
Around methods are accepted:
Note that these operators don’t support before
, after
and around
methods (indeed, there is no room for them anymore). They do supportaround methods, where call-next-method
is allowed, but they don’tsupport calling call-next-method
in the primary methods (it wouldindeed be redundant since all primary methods are called, or clunky tonot call one).
CLOS allows us to define a new operator as a method combination type, beit a lisp function, macro or special form. We’ll let you refer to thebooks if you feel the need.
Debugging: tracing method combination
It is possible to trace the methodcombination, but this is implementation dependent.
In SBCL, we can use (trace foo :methods t)
. See this post by an SBCL core developer.
Github Slots Free
For example, given a generic:
Let’s trace it:
MOP
We gather here some examples that make use of the framework providedby the meta-object protocol, the configurable object system that rulesLisp’s object system. We touch advanced concepts so, new reader, don’tworry: you don’t need to understand this section to start using theCommon Lisp Object System.
We won’t explain much about the MOP here, but hopefully sufficientlyto make you see its possibilities or to help you understand how someCL libraries are built. We invite you to read the books referenced inthe introduction.
Metaclasses
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
:
But we’ll change it to one of our own, so that we’ll be able tocount the creation of instances. This same mechanism could be usedto auto increment the primary key of a database system (this ishow the Postmodern or Mito libraries do), to log the creation of objects,etc.
Our metaclass inherits from standard-class
:
The :metaclass
class option can appear only once.
Actually you should have gotten a message asking to implementvalidate-superclass
. So, still with the closer-mop
library:
Now we can control the creation of new person
instances:
See that an :after
qualifier is the safest choice, we let thestandard method run as usual and return a new instance.
The &key
is necessary, remember that make-instance
is given initargs.
Now testing:
It’s working.
Controlling the initialization of instances (initialize-instance)
To further control the creation of object instances, we can specialize the methodinitialize-instance
. It is called by make-instance
, just aftera new instance was created but wasn’t initialized yet with thedefault initargs and initforms.
Github Slots App
It is recommended (Keene) to create an after method, since creating aprimary method would prevent slots’ initialization.
A typical example would be to validate the initial values. Here we’llcheck that the person’s name is longer than 3 characters:
Github Slate Theme
So this call doesn’t work anymore:
We are prompted into the interactive debugger and we are given achoice of restarts (continue, retry, abort).
So while we’re at it, here’s an assertion that uses the debuggerfeatures to offer to change “name”:
We get:
Github Slots Game
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:
Github Slatejs
See more in the books !
Page source: clos.md