Common Lisp/Advanced topics/CLOS
Common Lisp Object System (CLOS) is a part of Common Lisp that allows to use object-oriented programming techniques in your programs. It defines concepts such as object, class and method and their interaction. CLOS is the most powerful object system available in any programming language, and mastering the more peculiar aspects of it may take time. Fortunately, it is not necessary to be a CLOS expert to use it.
Two orthogonal concepts of CLOS are classes and generic functions. Let's start with the first one...
Classes and objects
[edit | edit source]A class is a "template" that describes the structure and behavior of its instances. Every kind of Lisp data is an instance of some class. There are built-in classes, such as the class of integers, or the class of strings. You can use the function class-of to determine the class of some Lisp object.
(class-of 5) => #<BUILT-IN-CLASS INTEGER> (class-of "aaaa") => #<BUILT-IN-CLASS STRING>
The results may be printed differently in your particular implementation, but the idea is the same: #< >
is Lisp syntax for unreadable data, which doesn't mean that it's unreadable to humans, but to the Lisp reader. It's easy to determine that 5 is an instance of the built-in class integer and "aaaa" is an instance of the built-in class string.
Built-in classes are usually not very interesting. But there is a way to create user-defined classes, and it's achieved with the defclass macro. Each instance of a user-defined class would have a number of slots which could contain various Lisp data. Let's see a typical use of defclass:
(defclass book () ((author :initarg :author :initform "" :accessor author) (title :initarg :title :initform "" :accessor title) (year :initarg :year :initform 0 :accessor year)) (:documentation "Describes a book"))
It has the following structure:
(defclass name (superclasses) (slots) options)
We'll ignore superclasses for now, let's look at slots instead. Each slot has a name and several options attached to it. The options are:
:initarg
- a keyword what would be used to supply slot values when an instance of a class is created.:initform
- if no value for a slot was supplied, it would be initialized with the result of evaluating initform. If there is no initform, an error would be signaled.:reader
- specifies a function to read a particular slot.:reader aaa
means: create a function aaa so that (aaa instance) would return the value of the slot.:writer
- specifies a function to write to a particular slot.:writer bbb
means: create a function bbb so that (bbb value instance) would set a slot of an instance to value.:accessor
- specifies a function to read and write a value of a slot.:accessor foo
means: create a function foo and create a function (setf foo) so that (foo instance) reads a value of a slot and (setf (foo instance) value) sets a value of a slot.- Note:
:reader foo :writer foo
is wrong! You need different names for reader and writer. Use:accessor foo
instead.
- Note:
:documentation
- provide a documentation string for a specific slot.
Every option is entirely optional, however at least one of :initarg
or :initform
should be supplied to be able to initialize a slot at the time of object creation. Failure to do so would result in a runtime error.
As for class options, the only one that's useful is :documentation
, which provides a documentation string for the whole class.
Now that we have a class, we can create some instances of it. Everything in Lisp is an object, but the instances of a standard class, such as the one defined by defclass above, are called standard objects. For the remainder of this section the word object refers to standard object.
How to make a new object? A function make-instance is what we need:
(setf *my-book* (make-instance 'book))
Now *my-book*'s value is an object of the class book. The first argument to make-instance may evaluate either to the class itself (such as a result of class-of call) or the symbol that names a class (such as a result of quoting this symbol). In this case we'll use the symbol, which is much easier to produce.
Let's look at *my-book*:
(class-of *my-book*) => #<STANDARD-CLASS BOOK> (author *my-book*) => "" (year *my-book*) => 0
The book is rather bland right now. Its fields are set to default class values, but it's easy to change them:
(setf (title *my-book*) "ANSI Common Lisp") (setf (author *my-book*) "Paul Graham") (setf (year *my-book*) 1995)
This is because an appropriate accessor functions were set up in the class definition. In general case accessing slots of an object is somewhat harder. For example, if only reader functions were defined, you are still able to change slots, using slot-value function:
(setf (slot-value *my-book* 'year) 1995) (year *my-book*) => 1995
You can ditch reader functions as well and read slot values using slot-value function too. This however doesn't add readability to your code.
Most of the time you don't want to create an object with all slots set to default values. For example, if you want to make an object representing a specific book, you already know its title, author and year, and you want to initialize a new object with these values, not some useless empty strings. If there is an :initarg option specified in the slot definition, this slot can be initialized with a user-specified value at the time of the object's creation. It is possible by supplying a corresponding keyword argument to make-instance:
(make-instance 'book :author "Paul Graham" :title "ANSI Common Lisp" :year 1995)
Since the default values for book slots are always useless (there is no year 0, and there is no book with an empty title), the :initform slot options should be removed. Then, a user who forgets to initialize a slot will get an error, and will fix it by providing all necessary information next time. It is also wise to provide only :reader functions for slots that are not intended to change (this is the case with every slot of our book class). In short, provide only those options that could be useful.