04-02-2021



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.Github Slots

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-SLOTcondition:

slot-value is setf-able:

Initial and default values (initarg, initform)

  • :initarg :foo is the keyword we can pass to make-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 the defclass.

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 is setf-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:

Slatejs

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:

arrayhash-tablereadtable
bit-vectorintegerreal
broadcast-streamlistsequence
characterlogical-pathnamestream
complexnullstring
concatenated-streamnumberstring-stream
conspackagesymbol
echo-streampathnamesynonym-stream
file-streamrandom-statet
floatratiotwo-way-stream
functionrationalvector

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-classMay not use make-instance, may not use slot-value, may not use defclass to modify, may not create subclasses.
structure-classMay 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-classNone 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 defgenerics. 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 personor 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 or rest arguments to an existing genericmethod’s lambda list, you will need to delete its declaration withfmakunbound (or C-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 of declare ignore.)

  • Each defmethod form generates (and returns) a CLOSinstance, of class standard-method.

  • An eql specializer won’t work as is with strings. Indeed, stringsneed equal or equalp to be compared. But, we can assign our stringto a variable and use the variable both in the eql 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:

  1. compute the list of applicable methods
  2. if no method is applicable then signal an error
  3. sort the applicable methods in order of specificity
  4. 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 :aroundqualifiers:

  • (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:

  1. compute the applicable methods, and partition them intoseparate lists according to their qualifier;
  2. if there is no applicable primary method then signal anerror;
  3. sort each of the lists into order of specificity;
  4. execute the most specific :around method andreturn whatever that returns;
  5. if an :around method invokescall-next-method, execute the next most specific:around method;
  6. 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 to call-next-method or next-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 to call-next-method or next-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-combinationoption 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 aroundmethods (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