Common Lisp Object System (CLOS)

20 min read
Updated
Suggest editHistoryTalk
RawGraph

Last edited

Fact-checked

In review queue

Sources

15 citations

Revision

v2 · 4,094 words

Fact-checks are independent of edits: a reviewer re-verifies the article against its sources and stamps the date. How we verify

The Common Lisp Object System (CLOS, often pronounced "see-loss" or "kloss") is the object-oriented programming facility built into ANSI Common Lisp, organized around generic functions and multiple dispatch rather than message passing to a privileged receiver. It was added to the language by a vote of the X3J13 standards committee on June 15, 1988, and is normative in ANSI INCITS 226-1994 (formerly ANSI X3.226-1994), the formal Common Lisp standard [1][4]. CLOS supports multiple inheritance with a deterministic class precedence list, allows complex method combinations (including before, after, and around methods), and exposes a Metaobject Protocol that lets programmers reach into and modify the implementation of the object system itself [2]. In short, CLOS is "the facility for object-oriented programming in ANSI Common Lisp," and is unusual in that any method can specialize on more than one argument, so a single call dispatches on the runtime classes of all of its specialized arguments at once [10].

CLOS grew out of the Lisp object systems used in 1980s artificial intelligence research, especially Flavors and New Flavors at the MIT AI Lab and Symbolics, and CommonLoops at Xerox PARC [5][6]. It influenced later languages with multiple dispatch, including Dylan, Julia, and the multimethods in Clojure. Gregor Kiczales, one of CLOS's principal authors, would go on from work on the Metaobject Protocol to develop aspect-oriented programming and AspectJ at Xerox PARC in the late 1990s [8].

What is CLOS?

A CLOS program is built from four kinds of entities: classes, slots, generic functions, and methods. Classes describe the shape of objects (their slots, which are roughly equivalent to fields in other languages). Generic functions are named operations whose actual behavior depends on the classes of all of their arguments, not just the first one. Methods are pieces of code attached to a generic function and specialized on the classes of one or more arguments. When a generic function is called, CLOS picks the applicable methods, sorts them by specificity, and combines them into an effective method using a method combination strategy.

This is a different shape from the more common object model in Smalltalk, C++, Python, or Java, where a method belongs to a class and is invoked by sending a message to an object: obj.method(args). In CLOS, a generic function is a stand-alone function. There is no receiver. Calling (area shape) looks up the most specific applicable method based on the class of shape; calling (intersect circle rectangle) looks it up based on the classes of both circle and rectangle. The dispatch rule is symmetric across arguments.

CLOS is part of the standard. Any conforming Common Lisp implementation provides it, including SBCL, Clozure CL (CCL), ECL, ABCL, CLISP, GCL, LispWorks, and Allegro Common Lisp. Historically it was also available on Symbolics Genera and on Lucid Common Lisp.

When was CLOS created, and where did it come from?

Predecessors: Flavors, New Flavors, and CommonLoops

Lisp had been used for object-oriented programming since the 1970s. The most influential of the early systems was Flavors, developed by Howard Cannon at the MIT Artificial Intelligence Laboratory for the Lisp Machine and Lisp Machine Lisp. Flavors was the first widely used language to feature mixins. It used message passing in the Smalltalk style, but with multiple inheritance, before-daemons and after-daemons, and other features that were unusual for their time [6]. Symbolics adopted Flavors and built much of the Genera operating system on top of it.

Xerox PARC, in parallel, was experimenting with LOOPS (Lisp Object-Oriented Programming System) and then CommonLoops, presented at OOPSLA in 1986 by Daniel G. Bobrow, Kenneth Kahn, Gregor Kiczales, Larry Masinter, Mark Stefik, and Frank Zdybel [5]. CommonLoops is where the generic function model and the heavy use of metaobjects (objects representing classes, methods, and dispatchers) really took shape. As the paper's abstract put it, "CommonLoops blends object-oriented programming smoothly and tightly with the procedure-oriented design of Lisp," so that "message passing is invoked via normal Lisp function call" and "it is easy to incrementally move a program between the procedure and object-oriented styles" [5]. Generic functions are simply Lisp functions; there is no separate message-send protocol.

Between Flavors and CommonLoops sat New Flavors, a redesign of Flavors that replaced message passing with generic functions. This was the bridge: it convinced the Lisp Machine community that giving up the receiver-based model in favor of generic functions was workable.

The CLOS committee, 1986 to 1988

In 1986, the groups behind New Flavors and CommonLoops met to merge their designs into a single proposal for the X3J13 Common Lisp standardization committee. The principal authors of the resulting CLOS specification were Daniel G. Bobrow and Gregor Kiczales (both Xerox PARC), David A. Moon and Sonya E. Keene (both Symbolics), and Richard P. Gabriel and Linda G. DeMichiel (both Lucid) [1]. The early specification appeared as X3J13 Document 87-002 in 1987. The full specification, often referenced today, was X3J13 Document 88-002R, in three parts: Part 1 covered Programmer Interface Concepts, Part 2 the Functions in the Programmer Interface, and Part 3 the Metaobject Protocol [1].

At its meeting on June 15, 1988, X3J13 voted to adopt Parts 1 and 2 of the CLOS specification (the user-level language) into the draft Common Lisp standard. The adopting motion read: "The X3J13 Committee hereby accepts chapters 1 and 2 of the Common Lisp Object System, as defined in document 88-002R, for inclusion in the Common Lisp language being specified by this committee" [1]. Part 3, the Metaobject Protocol, was deliberately left out of the standard but kept available as a de facto extension implemented by most of the major vendors. The full ANSI Common Lisp standard, INCITS 226-1994 (X3.226-1994), included the CLOS programmer interface as a normative part of the language [4].

After standardization

In 1991, Kiczales, Jim des Rivières, and Bobrow published The Art of the Metaobject Protocol (MIT Press, ISBN 0-262-11158-6 hardcover, 0-262-61074-4 paperback) [2]. The book set out a formal specification of the Metaobject Protocol for CLOS and argued, more broadly, that exposing the design of an object system through reflective hooks is a useful programming language design technique in its own right. Sonya Keene's earlier Object-Oriented Programming in Common Lisp: A Programmer's Guide to CLOS (Addison-Wesley, 1989, ISBN 0-201-17589-4) remains the standard tutorial [3].

What are the core concepts of CLOS?

Classes and slots

Classes are defined with defclass. A class has a name, a list of direct superclasses, and a list of slot specifiers. Slots can have initial values, types, accessor functions, allocation modes (per-instance or per-class), and documentation. Slot accessors come in three flavors: a :reader defines a function that reads the slot, a :writer defines a function that writes it, and an :accessor defines both with the read function and a (setf accessor) method.

(defclass animal ()
  ((name :initarg :name :reader animal-name)
   (mass :initarg :mass :accessor animal-mass)))

(defclass mammal (animal) ())

(defclass dog (mammal)
  ((breed :initarg :breed :accessor dog-breed)))

Classes are themselves first-class objects, instances of a metaclass (standard-class by default). The default base class is standard-object, which in turn inherits from t, the universal class. Almost every Common Lisp built-in type is also a class: integer, string, symbol, and so on are classes you can specialize methods on.

Generic functions and methods

A generic function is declared with defgeneric and acquires methods through defmethod.

(defgeneric describe-animal (animal))

(defmethod describe-animal ((a animal))
  (format t "~a weighs ~a kg.~%" (animal-name a) (animal-mass a)))

(defmethod describe-animal ((d dog))
  (call-next-method)
  (format t "It is a ~a.~%" (dog-breed d)))

The syntax ((a animal)) is a parameter specializer: this method applies when the first argument is an instance of animal. The dog method is more specific. Calling (describe-animal fido) invokes the more specific method, and call-next-method runs the next applicable method in the precedence order, much like a super call but generalized to arbitrary chains of methods.

How does multiple dispatch work in CLOS?

The big difference from Smalltalk-style object systems is that any method can specialize on more than one argument, and CLOS dispatches on all of them. As the standard puts it, "methods can be specialized upon any or all of their required arguments," in contrast to single-dispatch languages where "methods are only specialized on the first argument" [10]. A method definition like

(defmethod collide ((a asteroid) (s spaceship))
  ...)

(defmethod collide ((s spaceship) (a asteroid))
  ...)

(defmethod collide ((a asteroid) (b asteroid))
  ...)

defines three different methods on the same collide generic function, each picked based on the runtime classes of both arguments. This is what is meant by multiple dispatch. It is genuinely symmetric: there is no privileged "this" or "self".

Because methods belong to generic functions rather than to classes, there is also no syntactic distinction between "my class's method" and "someone else's method". Anyone can extend a generic function with new methods on their own classes, including methods that specialize on classes from third-party libraries. There is no private or protected; CLOS does not provide encapsulation by default. Slot access can be restricted by package conventions but is not enforced by the language.

Method combinations

When several methods are applicable to a call, CLOS combines them into an effective method according to a method combination type. The default is standard method combination, which uses four method qualifiers [10].

QualifierRoleOrder
:beforeSide-effecting setupAll applicable, most specific first
primary (no qualifier)The actual computationMost specific runs; call-next-method continues the chain
:afterSide-effecting teardownAll applicable, most specific last
:aroundWraps everythingMost specific runs first; usually delegates with call-next-method

The semantics: all :before methods run, in most-specific-first order. Then the most specific primary method runs (and may call call-next-method to invoke less specific primaries). Then all :after methods run, in most-specific-last order. The value of the primary method becomes the value of the call. If :around methods exist, the most specific :around runs first and gets full control: only when it calls call-next-method do the other methods run at all.

Beyond standard method combination, CLOS provides several short method combinations: +, and, or, list, append, nconc, min, max, progn. With + combination, for example, a call returns the sum of the values returned by the applicable methods. Programmers can also define entirely new method combinations with define-method-combination, which gives the language something close to user-defined operator overloading at the dispatch level.

Multiple inheritance and the class precedence list

CLOS supports multiple inheritance. When a class has more than one direct superclass, CLOS computes a class precedence list (CPL) that totally orders the class and all its ancestors. The CPL determines method specificity, slot inheritance, and the order in which call-next-method walks through methods.

The CLOS standard specifies an algorithm for computing the CPL that is essentially what was later renamed C3 linearization. The C3 algorithm itself was described in a 1996 paper by Kim Barrett, Bob Cassels, Paul Haahr, David A. Moon, Keith Playford, and P. Tucker Withington in the context of Dylan, formalizing the same intuition: a class always precedes its direct superclasses, and the order in which superclasses are listed in defclass is preserved (the local precedence order) [7]. C3 was later adopted as the default method resolution order in Python 2.3 (2003) and in Raku, among others [7].

The CPL is what makes diamond inheritance well-behaved in CLOS. Slots inherited along multiple paths are merged according to documented rules (most specific writer wins for :initform, all type constraints are intersected, and so on).

Object construction and lifecycle

Instances are created with (make-instance 'class-name :slot-name value ...). CLOS exposes the construction process as a chain of generic functions, each of which can be specialized: make-instance calls allocate-instance to create the storage, then initialize-instance, which by default calls shared-initialize. There is also reinitialize-instance for resetting slots after creation.

When a class is redefined while instances exist (a common situation in interactive Lisp development, since you are usually editing live code), CLOS does not invalidate the old instances. Instead, when you next touch one, CLOS calls update-instance-for-redefined-class, a generic function the programmer can specialize, to migrate the instance to the new shape. The same mechanism, via update-instance-for-different-class, supports change-class, which transmutes an existing instance into a member of a different class without copying.

What is the Metaobject Protocol?

The CLOS Metaobject Protocol (MOP) treats the building blocks of CLOS itself, classes, slot definitions, generic functions, and methods, as instances of metaclasses. As the reference description puts it, the MOP "defines a standard interface to the underpinnings of the CLOS implementation, treating classes, slot-descriptions, generic-functions and methods themselves as instances of metaclasses" [10]. Programmers can subclass standard-class, standard-generic-function, or standard-method and override the generic functions that the implementation uses internally to lay out instances, dispatch calls, compute method applicability, walk the class precedence list, and so on [2].

This is not a curiosity. The MOP is how a CLOS programmer adds capabilities the language designers never anticipated, without forking the implementation. Common uses include:

  • Persistence layers that subclass standard-class to intercept slot reads and writes, transparently mapping objects to a database or to disk.
  • Constraint and dependency tracking systems (the Cells library is the classic example) that turn ordinary slot accesses into dataflow events.
  • Foreign-object wrappers where an instance of a custom metaclass holds a foreign pointer and lays out its slots in C-compatible memory.
  • AOP-style instrumentation, where method dispatch is intercepted to add logging, tracing, security checks, or transactions across many functions at once.

The MOP was deliberately kept out of ANSI Common Lisp because it was considered too implementation-sensitive to standardize. In practice, every major Common Lisp implementation supports the MOP described in The Art of the Metaobject Protocol, with small portability gaps that the closer-mop library smooths over [2][13].

Reflective architecture

The MOP is reflective in the strict sense: the same generic functions you use to define ordinary classes are also the ones that the system uses internally. Calling (class-of obj) gives you a class object you can introspect. Calling (class-of (class-of obj)) gives you the metaclass. The hierarchy bottoms out: standard-class is itself an instance of standard-class, so the tower does not require infinite metalevels.

What is unusual about CLOS, compared to languages like Java or Python where reflection is bolted on, is that the reflective interface and the user-facing interface are the same. Specializing compute-applicable-methods does not require any special hook; it is just a method definition.

A small worked example

A small expression tree using inheritance, accessors, and multiple dispatch:

(defgeneric evaluate (expr env))

(defclass expression () ())

(defclass literal (expression)
  ((value :initarg :value :reader literal-value)))

(defclass variable (expression)
  ((name :initarg :name :reader variable-name)))

(defclass binary-op (expression)
  ((left  :initarg :left  :reader op-left)
   (right :initarg :right :reader op-right)))

(defclass plus (binary-op) ())
(defclass times (binary-op) ())

(defmethod evaluate ((e literal) env)
  (declare (ignore env))
  (literal-value e))

(defmethod evaluate ((e variable) env)
  (cdr (assoc (variable-name e) env)))

(defmethod evaluate ((e plus) env)
  (+ (evaluate (op-left e) env)
     (evaluate (op-right e) env)))

(defmethod evaluate ((e times) env)
  (* (evaluate (op-left e) env)
     (evaluate (op-right e) env)))

(defmethod evaluate :before ((e expression) env)
  (declare (ignore env))
  (format *trace-output* "Evaluating ~s~%" e))

The :before method runs before every call to evaluate and traces the expression. Because it specializes on the root class expression, it applies uniformly to all subclasses. To turn tracing off, you remove that one method; you do not have to change any of the primary methods.

How does CLOS differ from class-based object systems?

AspectSmalltalk-style OOP (Smalltalk, Java, Python, Ruby, C++)CLOS
Method dispatchSingle dispatch on receiverMultiple dispatch on all specialized arguments
Method ownershipMethods belong to classesMethods belong to generic functions
Receiver conceptself / this is privilegedSymmetric across arguments
InheritanceSingle (Java, Smalltalk) or multiple (C++, Python, Eiffel)Multiple, with C3-style class precedence list
Combining inherited methodssuper callcall-next-method, plus before/after/around qualifiers, plus user-defined method combinations
EncapsulationPer-class private/protected/publicNone enforced; convention only
Class as objectVaries (rich in Smalltalk and Python, weak in Java and C++)Yes: classes are first-class instances of metaclasses
ReflectionOften bolt-on, separate APISame API as the user-facing language; the MOP is just CLOS
Adding methods to existing classesOften forbidden or awkwardRoutine; methods can specialize on built-in or third-party classes

The absence of encapsulation is a real design choice and not an oversight. CLOS treats classes as data definitions and generic functions as operations on data; it does not bundle them. This is closer in spirit to abstract data types in functional languages than to message-sending OO. It also means CLOS programs are usually more like Lisp programs with a class system attached than they are like Java programs translated into Lisp.

Which Lisp implementations provide CLOS?

CLOS is part of the ANSI standard, so every conforming Common Lisp provides it [4]. The MOP is not standard but is implemented in some form by all of them.

ImplementationStatusCLOS / MOP notes
SBCL (Steel Bank Common Lisp)Free software, actively developedFull ANSI CLOS, full AMOP-style MOP
CCL (Clozure CL)Free software, MIT/Apache mixFull ANSI CLOS, full MOP
ECL (Embeddable Common Lisp)Free softwareFull CLOS, MOP with some gaps closed by closer-mop
ABCL (Armed Bear Common Lisp)Free software, runs on JVMFull CLOS, MOP largely supported
CLISPFree softwareFull CLOS, extensive MOP
GCL (GNU Common Lisp)Free softwareFull CLOS
LispWorksCommercial, Lispworks Ltd.Full CLOS, full MOP
Allegro Common LispCommercial, Franz Inc.Full CLOS, full MOP, AllegroCache extensions
Symbolics GeneraHistorical, Symbolics Inc.CLOS available alongside Flavors
Lucid Common LispHistorical, Lucid Inc.One of the original CLOS reference implementations

The closer-mop library by Pascal Costanza papers over differences in MOP support across implementations and is the usual way library code targets multiple Common Lisps when it needs metaprogramming [13].

How did CLOS influence later languages?

CLOS was developed inside what was, in the late 1980s, the dominant infrastructure for AI research. Common Lisp was the working language at Symbolics, at Xerox PARC, at Lucid, and in many university AI labs. Several ideas that originated or were polished in CLOS later spread well outside the Lisp world.

  • Dylan, originally developed at Apple Cambridge in the early 1990s, started as a CLOS-derived language with infix syntax. Dylan refined the class precedence list algorithm, and the C3 paper that resulted was written in the Dylan context [7].
  • Julia uses multiple dispatch as its core organizing principle. Methods are attached to generic functions, parameter specializers determine applicability, and the dispatch model is symmetric across arguments. Julia's designers cite CLOS and Dylan as direct ancestors, while noting a key difference: in Julia and Dylan "extensibility is the default" and the built-in functions "are all generic and extensible," whereas in CLOS not every function is a generic function [14].
  • Clojure ships multimethods built on generic functions and a user-supplied dispatch function, with derivation hierarchies that resemble CLOS class precedence. Method combinations in the CLOS sense have been added by libraries such as Methodical.
  • Aspect-oriented programming. Gregor Kiczales, after working on the CLOS MOP, led the team at Xerox PARC that introduced aspect-oriented programming in 1997 and went on to design AspectJ, an aspect-oriented extension of Java [8]. The intellectual line from MOP method combination to AOP "advice" is direct: before, after, and around methods are essentially the dispatch-time advice that AOP systems generalize across class hierarchies.
  • Method resolution order in Python. Python 2.3 adopted C3 linearization for new-style classes after a 2002 essay by Michele Simionato analyzed it explicitly in CLOS and Dylan terms [7]. Python's functools.singledispatch, added in Python 3.4 via PEP 443, is a deliberately small subset of CLOS-style generic functions: it dispatches on the type of the first argument only, whereas CLOS dispatches on any argument [15].
  • Other languages. Perl 6 / Raku adopted C3, Ruby has CLOS-inspired method-combination libraries, and the open-source CLOS-Like Object System (CLOPS) for Scheme and other ports keep showing up because the design is unusually portable.

What keeps CLOS in the conversation is not nostalgia but the fact that the basic design choices, generic functions instead of message passing, multiple dispatch, method combinations, classes as first-class metaobjects, were correct enough that languages designed thirty years later still adopt them one at a time.

What are the limitations and trade-offs of CLOS?

CLOS is not without rough edges. Its performance model is harder to reason about than single-dispatch OO: dispatch cost depends on the number of applicable methods and on the method combination, and adding a method anywhere can invalidate a dispatch cache. The lack of built-in encapsulation means programmers have to build conventions on top, and the conventions vary across teams. Method combinations and :around methods are powerful but can make it hard to read a generic function call in isolation; you may need to inspect every applicable method to know what actually runs. The MOP is a sharp tool: subclassing standard-class casually can break compiler optimizations, and AMOP itself spends considerable space on what can and cannot be customized without breaking other parts of the protocol [2].

ELI5: CLOS in plain terms

In most languages, an object owns its actions: you tell a dog object to bark(), and the dog "has" the bark. CLOS flips this around. The action (called a generic function) lives on its own, and you write separate little rules (methods) that say "when the argument is a dog, do this" or "when one argument is an asteroid and the other is a spaceship, do that." Lisp then looks at the actual things you passed in and automatically picks the rule that fits best. Because the action can look at all of its arguments, not just the first one, CLOS can express interactions between two objects (like a collision) naturally, which most object systems make awkward.

See also

References

  1. Bobrow, D. G., DeMichiel, L. G., Gabriel, R. P., Keene, S. E., Kiczales, G., and Moon, D. A. (1988). *Common Lisp Object System Specification*. X3J13 Document 88-002R. Republished in *Lisp and Symbolic Computation*, Volume 1, Number 3-4, January 1989.
  2. Kiczales, G., des Rivières, J., and Bobrow, D. G. (1991). *The Art of the Metaobject Protocol*. MIT Press. ISBN 0-262-11158-6 (hardcover), 0-262-61074-4 (paperback).
  3. Keene, S. E. (1989). *Object-Oriented Programming in Common Lisp: A Programmer's Guide to CLOS*. Addison-Wesley. ISBN 0-201-17589-4.
  4. American National Standards Institute (1994). *American National Standard for Information Technology: Programming Language, Common Lisp*. ANSI INCITS 226-1994 (formerly ANSI X3.226-1994).
  5. Bobrow, D. G., Kahn, K., Kiczales, G., Masinter, L., Stefik, M., and Zdybel, F. (1986). "CommonLoops: Merging Lisp and Object-Oriented Programming." *OOPSLA '86 Conference Proceedings*, ACM SIGPLAN Notices, Volume 21, Number 11, pages 17-29.
  6. Moon, D. A. (1986). "Object-Oriented Programming with Flavors." *OOPSLA '86 Conference Proceedings*.
  7. Barrett, K., Cassels, B., Haahr, P., Moon, D. A., Playford, K., and Withington, P. T. (1996). "A Monotonic Superclass Linearization for Dylan." *OOPSLA '96 Conference Proceedings*.
  8. Kiczales, G., Lamping, J., Mendhekar, A., Maeda, C., Lopes, C., Loingtier, J.-M., and Irwin, J. (1997). "Aspect-Oriented Programming." *Proceedings of ECOOP '97*. Springer LNCS 1241.
  9. Pitman, K., editor (1994). *Common Lisp HyperSpec*. Online standard reference. Sections on Objects, defclass, defgeneric, defmethod.
  10. Wikipedia. "Common Lisp Object System." https://en.wikipedia.org/wiki/Common_Lisp_Object_System
  11. Wikipedia. "Flavors (programming language)."
  12. Wikipedia. "The Art of the Metaobject Protocol."
  13. Costanza, P. "Closer to MOP: A compatibility layer for the CLOS Metaobject Protocol." Software, available at https://github.com/pcostanza/closer-mop.
  14. Wikipedia. "Julia (programming language)." https://en.wikipedia.org/wiki/Julia_(programming_language)
  15. Python Software Foundation. "PEP 443: Single-dispatch generic functions" and *functools* documentation (singledispatch, added in Python 3.4). https://peps.python.org/pep-0443/

Improve this article

Add missing citations, update stale details, or suggest a clearer explanation. Every suggestion is reviewed for sourcing before it goes live.

1 revision by 1 contributors · full history

Suggest edit