Common Lisp Object System
The Common Lisp Object System (CLOS) is a dynamic, extensible object-oriented programming system integrated into the Common Lisp language, enabling the definition of classes, generic functions, and methods that support multiple dispatch, inheritance, and customizable behavior through a meta-object protocol.[1] It treats classes and generic functions as first-class objects, allowing seamless integration with Common Lisp's functional and symbolic paradigms while providing mechanisms for polymorphism and code reuse.[2]
CLOS emerged from collaborative efforts in the mid-1980s to unify disparate object-oriented extensions in Lisp implementations, including Symbolics' New Flavors and Xerox PARC's CommonLoops.[1] Development began in 1986 under the auspices of the X3J13 committee, which was tasked with standardizing Common Lisp; a draft specification was completed by March 1987 and endorsed for inclusion in the ANSI standard.[1] The system was formally incorporated into the ANSI INCITS 226-1994 standard for Common Lisp, published on December 8, 1994, making it the first standardized object-oriented extension for a major programming language.[3]
Key features of CLOS include generic functions as the core dispatching mechanism, which enable methods to be specialized on multiple arguments rather than single objects, decoupling operations from specific classes.[2] It supports multiple inheritance via a class precedence list that resolves conflicts deterministically, along with flexible method combination strategies such as :before, :after, and :around qualifiers to compose behaviors.[1] The meta-object protocol (MOP) allows programmers to introspect and modify the system's internals at runtime, facilitating advanced customizations like domain-specific languages or alternative inheritance models.[2] These elements make CLOS highly influential, bridging functional and object-oriented programming while supporting interactive development and runtime extensibility.[1]
Introduction
Overview and Purpose
The Common Lisp Object System (CLOS) is the native object-oriented programming extension defined in the ANSI Common Lisp standard, which was adopted in 1994. It offers a dynamic and extensible approach to object-oriented programming, allowing developers to define classes, methods, and behaviors that integrate deeply with Lisp's core features.[4] Developed primarily by Gregor Kiczales at Xerox PARC and a team from multiple organizations, including Daniel G. Bobrow (Xerox PARC), Linda G. DeMichiel and Richard P. Gabriel (Lucid), and Sonya E. Keene and David A. Moon (Symbolics), CLOS emerged from efforts to unify diverse object systems within the Lisp community.[5]
The primary purposes of CLOS include enabling multiple dispatch, where method selection depends on the types of all arguments rather than just the receiver, and supporting runtime changes to classes, methods, and even the object system itself.[6] It also facilitates tight integration with Common Lisp's symbolic computation paradigm, treating programs as manipulable data structures to promote incremental development and extensibility.[4] By marrying object-oriented concepts like inheritance and encapsulation with Lisp's functional and procedural abstractions, CLOS aims to enhance modularity, code reuse, and adaptability in complex software systems.[6]
In contrast to static object-oriented systems, CLOS emphasizes runtime evaluability, with no fixed compile-time bindings for method dispatch or class structures, allowing all elements to be inspected and modified dynamically during program execution.[4] This fully dynamic model supports Lisp's interactive development style but differs markedly from compile-time oriented languages, where polymorphism and inheritance are largely resolved before runtime.[6] At its core, generic functions serve as the dispatch mechanism, enabling polymorphic behavior across multiple argument types in a seamless extension of Lisp's existing type-generic operations.[6]
Integration with Common Lisp
The Common Lisp Object System (CLOS) is deeply integrated with Common Lisp's foundational features, particularly its treatment of functions and symbols as first-class citizens. Generic functions in CLOS are ordinary Common Lisp functions that can be passed as arguments, returned from other functions, or stored in data structures, enabling dynamic dispatch and extensibility without disrupting Lisp's functional paradigm. Classes in CLOS are also first-class objects, represented as instances of metaclasses such as standard-class, and are typically named by symbols via the defclass macro; the function find-class retrieves a class object from its symbol name, while class-name returns the associated symbol, allowing classes to be manipulated programmatically like any other Lisp object. This design ensures that CLOS constructs coexist seamlessly with Lisp's symbolic computation model, where symbols serve as identifiers for both traditional variables and object-oriented entities.
CLOS slots, which store instance data, integrate with Common Lisp's access mechanisms through functions like slot-value, which retrieves or sets the value of a named slot in an object using a symbol as the slot name.[7] This mirrors the use of property lists (plists) on symbols for ad-hoc data storage in non-OCLOS code, but slot-value provides a more structured and efficient approach for class instances, invoking slot-missing if the slot does not exist or slot-unbound if it is unbound.[7] For convenience, the with-slots macro allows temporary bindings of slot values as lexical variables within a lexical scope, akin to let for regular variables, facilitating readable access in methods without repeated calls to slot-value.[7] Accessors defined in defclass (e.g., via :accessor) generate generic functions that wrap slot-value calls, blending slot access with Lisp's generic function extensibility while preserving compatibility with existing plist-based code for symbols.
Error handling in CLOS operations leverages Common Lisp's condition system, where all predefined condition types, including CLOS-specific ones, are defined as CLOS classes inheriting from the condition class.[8] For instance, when no method applies to a generic function call, the no-applicable-method condition is signaled, allowing handlers to intervene via handler-case or restarts, much like general Lisp errors. Similarly, no-next-method within method bodies signals a condition to access superclass behavior, integrating object-oriented error recovery with Lisp's non-local exit and restart mechanisms for robust, interactive debugging.
A key aspect of CLOS's interoperability is the ability to define methods on Common Lisp's built-in types, such as number or list, by using them directly as parameter specializers in defmethod. This allows object-oriented extensions to core Lisp data without altering their fundamental behavior. For example, one can specialize a generic function cook on the built-in class number for the time argument:
lisp
(defmethod cook ((item food) (time number))
(format t "~&Cooking ~S for ~S seconds~%" item time))
(defmethod cook ((item food) (time number))
(format t "~&Cooking ~S for ~S seconds~%" item time))
Here, number acts as a class specializer, dispatching the method for any numeric time value, and integrates with Lisp's type system via predicates like numberp.[9] Another example extends printing for lists:
lisp
(defmethod print-object ((obj list) stream)
(format stream "#~S" (length obj)))
(defmethod print-object ((obj list) stream)
(format stream "#~S" (length obj)))
Such methods can be mixed freely with non-OOP code, as generic functions are invoked like regular functions, enabling gradual adoption of object-oriented patterns in existing Lisp programs.[9]
Core Components
Classes and Instances
In the Common Lisp Object System (CLOS), classes are defined using the defclass macro, which specifies the structure of objects by naming the class, its direct superclasses, and its slots. The syntax of defclass is (defclass class-name ({superclass-name}*) ({slot-specifier}*) &rest class-options), where class-name is a non-nil symbol denoting the new class, {superclass-name}* is a list of zero or more superclass symbols, {slot-specifier}* lists the slots, and class-options include options such as :default-initargs for default initialization arguments, :[documentation](/page/Documentation) for a string description, and :[metaclass](/page/Metaclass) to specify a custom metaclass.[10] This macro creates a new class object and establishes it as a type of the same name, enabling inheritance of slots and methods from superclasses while defining new slots local to the class.[10]
Slots in a defclass form are specified either as a simple slot-name symbol or as a list (slot-name &rest slot-options), where slot-options control access and initialization. Common slot options include :initarg initarg-name to bind the slot during instance creation via keyword arguments (multiple allowed), :initform form to provide a default value if no initarg is supplied (once only), :reader reader-function-name to generate a reader method (multiple allowed), :writer writer-function-name for a writer method (multiple allowed), :accessor accessor-function-name for both reader and writer (multiple allowed), :allocation :instance or :class to determine if the slot is per-instance or shared across instances (defaults to :instance, once only), :type type-specifier to declare the slot's type (once only), and :documentation string for description (once only).[10] Duplicate slot names or invalid options signal a program error, and the macro generates appropriate generic function methods for specified readers and writers.[10]
Instances of a class are created using the generic function make-instance, with syntax (make-instance class &rest initargs &key &allow-other-keys), where class is a class object or symbol naming a class, and initargs is a plist of initialization arguments.[11] This function allocates a new direct instance of the class and initializes its slots by processing initargs against the class's slot initargs and default-initargs, applying :initform values for unbound slots via a call to initialize-instance.[11] If class is a symbol, it is resolved to the class via find-class; invalid initargs signal an error, ensuring type safety during creation.[11]
Access to instance slots is provided by the function slot-value, with syntax (slot-value object slot-name), which returns the value of the named slot in object or signals an error via slot-missing if the slot does not exist.[7] The function supports modification via (setf (slot-value object slot-name) new-value), calling slot-missing with the new value if the slot is absent; for unbound slots, slot-unbound is invoked instead.[7] For more convenient lexical access, the with-slots macro establishes a lexical environment where slot names act as variables, with syntax (with-slots (slot-entry*) instance-form declaration* form*), translating references to calls of slot-value and supporting setf and setq for updates.[12] It is undefined to reference a non-existent slot, potentially signaling an error.[12]
The following example defines a simple point class with x and y slots, creates an instance, and accesses its slots:
lisp
(defclass point ()
((x :initarg :x :initform 0 :accessor point-x)
(y :initarg :y :initform 0 :accessor point-y)))
(defparameter *p* (make-instance 'point :x 3 :y 4))
(slot-value *p* 'x) ; => 3
(with-slots (x y) *p*
(incf x)
(incf y))
(point-x *p*) ; => 4
(point-y *p*) ; => 5
(defclass point ()
((x :initarg :x :initform 0 :accessor point-x)
(y :initarg :y :initform 0 :accessor point-y)))
(defparameter *p* (make-instance 'point :x 3 :y 4))
(slot-value *p* 'x) ; => 3
(with-slots (x y) *p*
(incf x)
(incf y))
(point-x *p*) ; => 4
(point-y *p*) ; => 5
This demonstrates class definition, instance creation with partial initargs (defaulting y to 0 initially), and slot access/modification using both direct and macro forms.[10][11][7][12]
Generic Functions and Methods
In the Common Lisp Object System (CLOS), generic functions provide a mechanism for polymorphic behavior, where the code executed depends on the classes or identities of the arguments supplied to the function. A generic function is defined using the defgeneric macro, which specifies the function's name, lambda list, and optional attributes such as argument precedence order or method combination type. The syntax is (defgeneric function-name gf-lambda-list [[option | {method-description}*]]), where gf-lambda-list outlines the parameter structure that all methods must conform to, ensuring congruence in required, optional, rest, key, and auxiliary parameters. Options include (:argument-precedence-order parameter-name+) to define dispatch priorities among arguments, (:method-combination method-combination ...) to specify how methods combine, and (:method-class method-class) to set the class of methods associated with the generic function.[13]
Methods for a generic function are defined using the defmethod macro, which associates specific implementations with particular argument types or values. The syntax is (defmethod function-name {method-qualifier}* specialized-lambda-list [[declaration* | documentation]] form*), where method-qualifier can be symbols like :before, :after, or :around to indicate the method's role in combination, and specialized-lambda-list includes parameter specializers such as class names (e.g., (shape circle)) or EQL forms (e.g., (eql 42)). If no generic function exists for function-name, defmethod implicitly creates one with defaults, such as the standard-generic-function class and standard method combination. The lambda list must be congruent with the generic function's, meaning matching parameter kinds and no defaults in the generic that are absent in the method.[14]
CLOS employs multiple dispatch, selecting methods based on the classes of all arguments, unlike single-dispatch systems that consider only the first. When invoked, the generic function identifies applicable methods—those whose specializers match the classes of the arguments—using class precedence lists (CPLs) to determine inheritance-based compatibility. Applicable methods are then sorted by specificity: for each argument position, a method is more specific if its specializer is a proper superclass or equal to the argument's class in the CPL; ties across positions are resolved by the generic function's argument precedence order. This enables dispatch on multiple arguments simultaneously.[15]
The most specific applicable methods form the effective method under the standard method combination, which executes :before methods (least to most specific), the primary (most specific non-auxiliary) method, and :after methods (most to least specific), with :around methods wrapping the entire sequence if present. Within the primary method (or others), call-next-method invokes the next less-specific applicable method in the sorted list, passing all original arguments or a subset, allowing cooperative behavior across methods. If no applicable methods exist, the generic function signals a no-applicable-method condition.[15]
For example, consider computing the area of geometric shapes:
lisp
(defclass [rectangle](/page/Rectangle) ()
((height :accessor rectangle-height :initarg :height :initform 0)
(width :accessor rectangle-width :initarg :width :initform 0)))
(defclass [circle](/page/Circle) ()
((radius :accessor circle-radius :initarg :radius :initform 0)))
(defmethod area ((x [rectangle](/page/Rectangle)))
(* (rectangle-height x) (rectangle-width x)))
(defmethod area ((x [circle](/page/Circle)))
(* pi (expt (circle-radius x) 2)))
(defclass [rectangle](/page/Rectangle) ()
((height :accessor rectangle-height :initarg :height :initform 0)
(width :accessor rectangle-width :initarg :width :initform 0)))
(defclass [circle](/page/Circle) ()
((radius :accessor circle-radius :initarg :radius :initform 0)))
(defmethod area ((x [rectangle](/page/Rectangle)))
(* (rectangle-height x) (rectangle-width x)))
(defmethod area ((x [circle](/page/Circle)))
(* pi (expt (circle-radius x) 2)))
Here, (area (make-instance 'rectangle :height 3 :width 4)) dispatches to the rectangle method, yielding 12, while (area (make-instance 'circle :radius 5)) selects the circle method, yielding approximately 78.54. The dispatch relies on the classes of the single shape argument, demonstrating single-dispatch polymorphism extensible to multiple arguments.[16]
Key Features
Multiple Inheritance and Dispatch
The Common Lisp Object System (CLOS) supports multiple inheritance by allowing the :direct-superclasses option in defclass to specify an ordered list of zero or more superclasses, enabling a class to inherit structure and behavior from multiple parents simultaneously. This facilitates complex hierarchies where a subclass can combine features from disparate superclasses, such as a shape class inheriting from both drawable (providing rendering capabilities) and colored (providing color attributes). The order in which superclasses are listed in defclass establishes the local precedence order, which influences inheritance resolution.[17]
To resolve inheritance across the entire hierarchy without cycles or ambiguities, CLOS computes a class precedence list (CPL) for each class, which is a total ordering of the class and all its superclasses that respects local precedence orders. The CPL is determined algorithmically as follows: for a class C with direct superclasses C_1, \dots, C_n, the local precedence order is the sequence (C, C_1, \dots, C_n); this process recurses on all superclasses to form a transitive partial order R, and a topological sort of the set of all superclasses under R yields the CPL, with C first and the root class t last. If the partial order is inconsistent (e.g., due to cycles), an error is signaled during class finalization. This linearization ensures unambiguous slot inheritance and method lookup while preserving the specificity of subclasses over superclasses.[17][1]
CLOS extends single-dispatch paradigms through multimethods, where generic functions dispatch based on the classes of all required arguments, not just the first. For a generic function call with arguments a_1, \dots, a_m, applicable methods are those whose parameter specializers (classes or eql forms) match the classes of the arguments via the CPL: a method is applicable if, for each specialized parameter, the argument's class precedes the specializer in the argument's CPL or matches an eql specializer. The set of applicable methods is then sorted by precedence, comparing specializers left-to-right (or per :argument-precedence-order in defgeneric): for disagreeing class specializers, the method with the more specific (earlier in CPL) specializer precedes; eql specializers take precedence over classes. This enables type-based selection that considers combinations, such as specializing on a circle and rectangle for an intersection operation.[18][19][1]
If no methods are applicable, the generic function invokes no-applicable-method with the original arguments and generic function, allowing user-defined handling (e.g., signaling an error or providing a fallback); the default method signals an error. Ambiguities arise if the precedence rules fail to establish a total order among applicable methods (e.g., incomparable specializers), in which case an error is signaled to prevent undefined behavior. These mechanisms ensure robust multi-argument dispatch while integrating seamlessly with the inheritance model.[20][19]
The following example illustrates multiple inheritance and two-argument dispatch. Define classes with shape inheriting from drawable and colored:
lisp
(defclass drawable () ((position :initarg :position :initform '(0 0))))
(defclass colored () ((color :initarg :color :initform :black)))
(defclass shape (drawable colored) ())
(defclass drawable () ((position :initarg :position :initform '(0 0))))
(defclass colored () ((color :initarg :color :initform :black)))
(defclass shape (drawable colored) ())
The CPL for shape is (shape drawable colored t), respecting local order. Now define a generic function render dispatching on a shape and a surface (e.g., canvas or screen):
lisp
(defgeneric render (object surface))
(defmethod render ((s shape) (c canvas))
(format t "Rendering ~a on canvas at ~a with color ~a~%"
(class-name (class-of s)) (slot-value s 'position) (slot-value s 'color)))
(defmethod render ((s shape) (scr screen))
(format t "Optimized rendering ~a on screen at ~a with color ~a~%"
(class-name (class-of s)) (slot-value s 'position) (slot-value s 'color)))
(defgeneric render (object surface))
(defmethod render ((s shape) (c canvas))
(format t "Rendering ~a on canvas at ~a with color ~a~%"
(class-name (class-of s)) (slot-value s 'position) (slot-value s 'color)))
(defmethod render ((s shape) (scr screen))
(format t "Optimized rendering ~a on screen at ~a with color ~a~%"
(class-name (class-of s)) (slot-value s 'position) (slot-value s 'color)))
For an instance (make-instance 'shape :position '(10 20) :color :red) and a canvas surface, the first method applies due to matching specializers (shape canvas), selected over any less specific alternatives via CPL precedence. If called with incompatible arguments (e.g., a non-shape object), no-applicable-method is invoked.[18][17]
Method Combination and Slots
In the Common Lisp Object System (CLOS), method combination determines how applicable methods for a generic function are selected and invoked to form an effective method, controlling the order of execution and the computation of results.[21] The standard method combination, which is the default for generic functions of class standard-generic-function, supports primary methods (those without qualifiers) as the core of the effective method, augmented by auxiliary methods qualified with :before, :after, or :around.[21] Primary methods perform the main computation and can invoke less specific methods via call-next-method to enable progressive refinement.[22]
Standard method combination operates in two forms: short and long, specified via the :method-combination option in defgeneric. In the short form, only :before and primary methods are used, omitting :after methods for simpler control flow where post-processing is unnecessary.[21] The long form includes all qualifiers, executing :before methods first in most-specific-first order (from the most specific applicable class to the least), followed by the most specific primary method, and then :after methods in most-specific-last order (from least to most specific).[21] If :around methods are present, the most specific one wraps the entire combination, potentially invoking the next less specific method via call-next-method; without such invocation, auxiliary methods outside the :around are skipped.[21] An error is signaled if no primary method applies or if a method has multiple qualifiers.[21]
Auxiliary methods enhance modularity: :before methods handle pre-processing, such as validation or setup, without returning values that affect the primary result; :after methods perform cleanup or logging post-execution, receiving the primary's results as arguments; and :around methods provide full control, often for exception handling or timing, by optionally proceeding to the inner combination.[21] For custom orders beyond the standard, the macro define-method-combination allows programmers to specify new types, either in a simple form with an operator (e.g., and or or) or a complex form with Lisp code to build the effective method form from applicable methods. This facility enables domain-specific behaviors, such as ensuring a logging step precedes all primaries.
Slots in CLOS manage object state and are defined in defclass forms as lists starting with a slot name followed by options. Slot inheritance follows the class precedence list: a subclass inherits all slots from superclasses unless shadowed by a local definition with the same name, in which case the local options take precedence while preserving unspecified inherited options where applicable (e.g., :initarg accumulates).[23] Allocation specifies storage: :instance allocates per-instance (default, mutable across instances), while :class allocates shared across the class and subclasses, suitable for constants or caches.[24]
Access to slots is facilitated by options like :accessor (creates a generic function for reading and writing), :reader (read-only generic function), and :writer (write-only generic function), all of which dispatch on the instance for encapsulation.[24] The :initarg option binds the slot to an initialization argument during object creation via make-instance, allowing external specification while defaulting to :initform if unbound.[24] Direct access via slot-value is possible but bypasses generic functions, potentially violating encapsulation.[25]
The following example illustrates a custom method combination for logging, where a :log auxiliary method always precedes primary methods:
lisp
(define-method-combination log-combination
((log-methods (eql :log)) (primary-methods t))
((log-methods primary-methods)
`(progn
,@(mapcar #'(lambda (method) `(call-method ,method)) log-methods)
(call-method ,(first primary-methods)))))
(defgeneric perform-action (obj))
(defmethod perform-action :log ((obj t)) (format t "Logging action on ~A~%" obj))
(defmethod perform-action (obj) (format t "Performing action~%"))
(define-method-combination log-combination
((log-methods (eql :log)) (primary-methods t))
((log-methods primary-methods)
`(progn
,@(mapcar #'(lambda (method) `(call-method ,method)) log-methods)
(call-method ,(first primary-methods)))))
(defgeneric perform-action (obj))
(defmethod perform-action :log ((obj t)) (format t "Logging action on ~A~%" obj))
(defmethod perform-action (obj) (format t "Performing action~%"))
Invoking (perform-action "example") first logs the argument, then executes the primary.
For slots with inheritance, consider a base class and subclass:
lisp
(defclass vehicle () ((wheels :initform 4 :accessor wheels)))
(defclass car (vehicle) ((doors :initarg :doors :accessor doors :initform 4)
(wheels :initform 4 :allocation :instance))) ; Shadows but specifies explicitly
(defclass vehicle () ((wheels :initform 4 :accessor wheels)))
(defclass car (vehicle) ((doors :initarg :doors :accessor doors :initform 4)
(wheels :initform 4 :allocation :instance))) ; Shadows but specifies explicitly
An instance (make-instance 'car :doors 2) inherits wheels from vehicle but allows per-instance variation, with doors local and initializable via keyword.[24][23]
Dynamic Modification and Introspection
The Common Lisp Object System (CLOS) supports dynamic modification of classes and methods at runtime, allowing developers to redefine class structures and update existing instances without halting execution or recompiling code. When a class is redefined using defclass, the existing class object is updated to reflect the new definition, and instances are lazily modified through the generic function update-instance-for-redefined-class. This function is invoked automatically when an obsolete instance is accessed, enabling the transfer of slot values from the old class to the new one while initializing any added slots according to their :initform or initialization arguments. For example, if a new slot is added during redefinition, existing instances receive the default value upon next access, preserving program continuity.[26]
To change the class of a specific instance to a different class, the generic function change-class is used, which destructively modifies the instance and returns it. This operation retains slot values for slots present in both the old and new classes, initializes new slots per standard rules (Section 7.2 of the ANSI standard), and invokes update-instance-for-different-class for custom handling of the transition. Semantic challenges arise, such as potential inconsistencies in method applicability or compiler optimizations, so change-class should be avoided within methods that access slots directly.[27][28]
Methods can also be dynamically added or removed from generic functions using add-method and remove-method. The function add-method attaches a method object to a generic function, replacing any existing method with matching parameter specializers and qualifiers, while signaling an error if the method's lambda list is incompatible. Conversely, remove-method detaches a specified method from the generic function without error if the method is not found. These operations facilitate runtime extension of behavior without redefining the generic function itself.[29][30]
CLOS provides introspection functions to query object and class properties at runtime, supporting reflective programming. The function class-of returns the class of which an object is a direct instance, while type-of returns a type specifier for a type containing the object (for CLOS instances, this is typically the class name). The generic function compute-class-precedence-list determines the ordered list of superclasses for a given class, following the topological sort defined in the standard to resolve inheritance hierarchies. For slot introspection, functions like slot-definition-name (from the metaobject protocol) access properties of slot-definition objects, such as name, initform, and allocation type, obtained via class-direct-slots or class-slots.[31][32][17][33]
These dynamic and introspective capabilities are particularly beneficial in interactive Lisp environments, where developers can redefine classes or methods during a REPL session and immediately observe effects on live instances, accelerating prototyping and debugging without restarts.[34]
For instance, consider redefining a person class to add an age slot:
lisp
(defclass person ()
((name :initarg :name :accessor name)))
(defvar p (make-instance 'person :name "Alice")) ; Existing instance
;; Redefine to add age slot
(defclass person ()
((name :initarg :name :accessor name)
(age :initarg :age :initform 18 :accessor age)))
;; Access triggers update
(print (age p)) ; Outputs 18, slot initialized via :initform
(defclass person ()
((name :initarg :name :accessor name)))
(defvar p (make-instance 'person :name "Alice")) ; Existing instance
;; Redefine to add age slot
(defclass person ()
((name :initarg :name :accessor name)
(age :initarg :age :initform 18 :accessor age)))
;; Access triggers update
(print (age p)) ; Outputs 18, slot initialized via :initform
This example demonstrates how redefinition updates the instance transparently via update-instance-for-redefined-class, maintaining the object's identity and values.[34]
Foundations and Components
The Metaobject Protocol (MOP) serves as the extensible foundation of the Common Lisp Object System (CLOS), providing a programmatic interface to the internals of the object system for introspection and customization. Described in the seminal book The Art of the Metaobject Protocol by Gregor Kiczales, Jim des Rivieres, and Daniel G. Bobrow (1991), the MOP represents CLOS elements such as classes, generic functions, methods, and slots as first-class metaobjects, allowing their behavior to be defined and modified through generic functions specialized on these metaobjects.[35] Although not part of the ANSI Common Lisp standard, the MOP has been widely implemented across major CLOS systems, enabling consistent extensibility in practice.[36]
At its core, the MOP relies on generic functions to perform metaoperations that govern CLOS behavior, such as inheritance resolution and object creation. For instance, the generic function compute-class-precedence-list determines the linearization of a class's superclasses according to CLOS rules, returning a list of class metaobjects and signaling an error for forward-referenced classes if necessary; this can be overridden to customize precedence computation.[37] Similarly, make-instance creates and initializes instances, with methods specialized for metaclasses that handle allocation and slot filling per CLOS specifications.[38] These generic functions form the protocol's operational backbone, allowing meta-level modifications without altering the underlying language runtime.[35]
Key to the MOP's structure are metaclasses and metaobjects, which model CLOS entities at the meta-level. The metaclass standard-class, a subclass of class and inheriting from standard-object, serves as the default for user-defined classes, supporting operations like slot access for instance and class allocations.[39] The metaclass funcallable-standard-class, also a subclass of class, extends this for funcallable instances, enabling classes whose instances can be invoked as functions while maintaining standard inheritance and reinitialization.[40] Metaobjects represent classes (determining instance structure and behavior), methods (encapsulating specializers and functions), and slots (via direct-slot-definition for specifiers and effective-slot-definition for inherited slots).[41][42][43]
Through this architecture, the MOP empowers CLOS's introspection—such as querying class precedence or slot definitions—and customization, permitting the definition of new metaclasses or method combinations that integrate seamlessly with standard CLOS operations.[35] This design reflects the MOP's goal of treating the object system as programmable, fostering reusable abstractions beyond conventional object-oriented programming.[35]
Metaclasses in the Common Lisp Object System (CLOS) provide a mechanism for customizing the behavior of class creation, instance management, and method dispatch at the meta-level, allowing developers to tailor the object system to specific domain requirements. By defining new metaclasses, users can override standard protocols to alter how classes are constructed or how slots are accessed and initialized, extending CLOS beyond its default functionality without modifying the core language. This customization is achieved primarily through the :metaclass option in the defclass macro, which specifies a custom metaclass for a class definition, enabling specialized handling of metaobjects such as classes and slot definitions.
To define a new metaclass, one subclasses standard-class (the default metaclass for user-defined classes) and specializes relevant generic functions in the Metaobject Protocol (MOP). For instance, the defclass form accepts the :metaclass keyword, passing it to the underlying ensure-class generic function, which invokes methods on the specified metaclass to build the class metaobject. This allows interception and modification of class initialization, such as adding custom slot accessors or validation logic during class finalization.[44]
A key area of customization involves protocols for method dispatch, where generic functions like compute-applicable-methods-using-classes can be specialized on custom metaclasses to alter how methods are selected based on argument classes. This function, part of the method selection subprotocol, computes applicable methods using class information and can be overridden to implement non-standard dispatch rules, such as context-dependent ordering or additional applicability criteria, thereby enabling advanced features like dynamic method filtering.[45]
Practical customizations via metaclasses include implementing persistent objects, where a metaclass overrides instance storage and retrieval to persist CLOS instances across sessions by serializing slots to a database or file system while maintaining standard interface compatibility. Similarly, metaclasses facilitate aspect-oriented programming by intercepting method invocations or slot accesses to weave in cross-cutting concerns, such as logging or security checks, without altering primary class definitions.[46][47]
As an illustrative example, consider a simple metaclass that overrides slot initialization to enforce default values or validation for all instances of classes using it:
lisp
(defclass validated-slot-class (standard-class)
())
(defmethod initialize-instance :after ((class validated-slot-class) &key)
(dolist (slot (clos:class-slots class))
(when (and (clos:slot-definition-initfunction slot)
(not (clos:slot-definition-initargs slot)))
;; Example: Add validation to initfunction if needed
(let ((old-init (clos:slot-definition-initfunction slot)))
(setf (clos:slot-definition-initfunction slot)
([lambda](/page/Lambda) (new-value)
(if (numberp new-value)
(funcall old-init new-value)
([error](/page/Error) "Slot value must be a number"))))))))
;; Usage
(defclass validated-point ()
((x :initarg :x :accessor point-x)
(y :initarg :y :accessor point-y))
(:metaclass validated-slot-class))
(defclass validated-slot-class (standard-class)
())
(defmethod initialize-instance :after ((class validated-slot-class) &key)
(dolist (slot (clos:class-slots class))
(when (and (clos:slot-definition-initfunction slot)
(not (clos:slot-definition-initargs slot)))
;; Example: Add validation to initfunction if needed
(let ((old-init (clos:slot-definition-initfunction slot)))
(setf (clos:slot-definition-initfunction slot)
([lambda](/page/Lambda) (new-value)
(if (numberp new-value)
(funcall old-init new-value)
([error](/page/Error) "Slot value must be a number"))))))))
;; Usage
(defclass validated-point ()
((x :initarg :x :accessor point-x)
(y :initarg :y :accessor point-y))
(:metaclass validated-slot-class))
This metaclass customizes slot initialization post-class creation, ensuring numeric validation for coordinates in the validated-point class, demonstrating how MOP methods like initialize-instance can be specialized for fine-grained control.
Practical Applications and Examples
The Metaobject Protocol (MOP) enables practical customizations of the Common Lisp Object System (CLOS) in real-world applications, particularly through metaclasses that alter default behaviors without disrupting standard CLOS syntax.
In database object-relational mapping (ORM) systems, the MOP facilitates efficient data handling via custom metaclasses. For instance, the UCL-GLORP ORM uses MOP introspection on CLOS classes to automatically map slots to database columns and relations, supporting lazy loading where related object slots remain unbound after initial queries and are populated only on access by trapping unbound conditions and issuing targeted database fetches. This approach minimizes memory usage and query overhead in applications like data retrieval sets.[48]
MOP-based persistence libraries extend this to transparent object storage. One such library integrates with backends like Elephant to make CLOS objects persistent by declaring classes or instances as such, using metaclasses to manage serialization, deserialization, and storage abstraction while preserving transient object semantics. This supports dynamic schema evolution, such as adding slots during development, with automatic data migration to avoid service disruptions in prototyping-heavy environments like web-based catalogs.[46]
In GUI frameworks, the MOP powers visual extensions of CLOS. The OpenMusic (OM) environment introduces a Visual MOP (VMOP) that extends the standard MOP to graphical elements, allowing runtime inspection and modification of CLOS metaobjects through visual boxes representing classes, methods, and functions. These components handle user interactions and trigger computations, enabling domain-specific visual programming for music composition and signal processing.[49]
The MOP also underpins domain-specific languages (DSLs) by tailoring the object system to niche behaviors. Persistence libraries, for example, employ MOP metaclasses to compile DSL declarations into code that seamlessly handles object storage and retrieval, abstracting away low-level data management while integrating with CLOS reflection for arbitrary structures.[46]
Since 2008, the Closer to MOP project has advanced MOP portability by providing a compatibility layer that implements or corrects absent MOP features across implementations, as identified by MOP Feature Tests. Maintained by Pascal Costanza with contributions from implementers like Duane Rettig and Steve Haflich, it supports nine major systems including SBCL 2.5.10, CCL 1.13, ECL 24.5.10, and LispWorks 8.1.1, allowing developers to write reliable, cross-implementation MOP extensions for libraries and applications.[50]
A representative MOP application is a validation metaclass for slots, which enforces constraints on slot values during access. By specializing the metaclass and overriding generic functions like (setf slot-value-using-class), it intercepts writes to validate data before storage, such as ensuring numeric slots receive positive values. This pattern, drawn from MOP principles for customizing slot access, integrates validation directly into the object model without macros or after-methods, promoting reusable class designs. For example, a metaclass might define:
lisp
(defclass validating-direct-slot-definition (standard-direct-slot-definition)
(([validator](/page/Validator) :initarg :validator :initform nil)))
(defmethod direct-slot-definition-class ((class validating-class) &rest initargs)
(if (getf initargs :validation)
(find-class 'validating-direct-slot-definition)
(call-next-method)))
(defmethod (setf slot-value-using-class) :around (new-value (class validating-class)
object ([slotd](/page/Slotd) validating-direct-slot-definition))
(let (([validator](/page/Validator) (slot-definition-validator slotd)))
(if (or (null [validator](/page/Validator)) (funcall [validator](/page/Validator) new-value))
(call-next-method)
([error](/page/Error) "Invalid value ~A for slot ~A" new-value (slot-definition-name slotd)))))
(defclass validating-direct-slot-definition (standard-direct-slot-definition)
(([validator](/page/Validator) :initarg :validator :initform nil)))
(defmethod direct-slot-definition-class ((class validating-class) &rest initargs)
(if (getf initargs :validation)
(find-class 'validating-direct-slot-definition)
(call-next-method)))
(defmethod (setf slot-value-using-class) :around (new-value (class validating-class)
object ([slotd](/page/Slotd) validating-direct-slot-definition))
(let (([validator](/page/Validator) (slot-definition-validator slotd)))
(if (or (null [validator](/page/Validator)) (funcall [validator](/page/Validator) new-value))
(call-next-method)
([error](/page/Error) "Invalid value ~A for slot ~A" new-value (slot-definition-name slotd)))))
Classes using this metaclass specify :validation in slot initargs, e.g., (:validation (lambda (x) (and (numberp x) (> x 0)))) for positive numbers.
In contemporary adoption, the MOP integrates with web frameworks like Hunchentoot by enabling dynamic CLOS-based request handlers and session objects with custom meta-behaviors for state management. Similarly, in AI libraries, it supports knowledge-based systems through metaclasses that customize object introspection for symbolic representations and rule engines.[51]
Historical Development
Predecessors and Influences
The Common Lisp Object System (CLOS) emerged from a lineage of object-oriented programming extensions developed within the Lisp community during the 1970s and 1980s. One of its primary predecessors was Flavors, an early object-oriented system implemented on the MIT Lisp Machine. Developed by Howard I. Cannon in 1979, Flavors introduced key concepts such as multiple inheritance, where classes could inherit from multiple superclasses forming a non-hierarchical lattice structure, and message-passing semantics, in which objects responded to messages by invoking methods.[52] These features allowed for modular combination of behaviors through mixins—reusable class extensions—and supported dynamic method combination via daemons (before, after, and primary methods), enabling runtime modifications that influenced CLOS's emphasis on flexibility and introspection.[52]
New Flavors, developed in the early 1980s by Symbolics as a successor to the original Flavors system, advanced these ideas by introducing generic functions that allowed method dispatch without explicit message passing, treating operations more like Lisp functions. This evolution emphasized integration with Lisp's functional style and influenced the design of CLOS through collaborative efforts with the CommonLoops team.[1]
At Xerox PARC, the LOOPS (Lisp Object-Oriented Programming System) project, initiated around 1982, extended Interlisp with object-oriented capabilities tailored for knowledge-based and expert systems. LOOPS integrated classes, messages, and procedural attachments, blending object-oriented paradigms with rule-based and data-directed programming, and provided precursors to generic operations through its support for polymorphic methods.[53] This system, detailed in a 1983 publication by Mark Stefik and colleagues, emphasized seamless integration of multiple programming styles within Lisp, laying groundwork for the multi-paradigm approach in later systems like CLOS.[53]
Building directly on Flavors and LOOPS, CommonLoops—developed starting around 1983 by Gregor Kiczales and collaborators at Xerox PARC—introduced generic functions as a core abstraction, allowing methods to be dispatched based on multiple arguments (multimethods) rather than single-receiver message passing.[54] This 1986 system, authored by Daniel G. Bobrow, Kenneth Kahn, Kiczales, Larry Masinter, Mark Stefik, and Frank Zdybel, also featured meta-objects for classes and methods, serving as an early precursor to the Metaobject Protocol (MOP) by enabling programmatic customization of object behavior.[54] The portable implementation, Portable CommonLoops (PCL), prototyped many CLOS features and was used as a reference during standardization, borrowing Flavors' dynamicity while advancing CommonLoops' multimethod dispatch.[54]
Standardization Process
The standardization of the Common Lisp Object System (CLOS) occurred primarily through the efforts of the ANSI X3J13 committee, formed in 1986 to develop a formal standard for Common Lisp. This committee, comprising representatives from various Lisp vendors and researchers, addressed the need for a unified object-oriented extension amid growing divergences in proprietary implementations. A key aspect of this process was the integration of the Portable Common Loops (PCL) system, a portable prototype developed by Xerox PARC researchers in the mid-1980s, which served as a reference implementation and heavily influenced the CLOS design. The Objects subcommittee, led by figures such as Daniel G. Bobrow and Gregor Kiczales, refined PCL's concepts—including multiple inheritance, generic functions, and method combination—into a cohesive specification over several years of iterative reviews and proposals. By 1988, the committee had produced a detailed draft in X3J13 Document 88-002R, which outlined the programmer interface and laid the groundwork for CLOS's inclusion in the broader Common Lisp standard.[55][56]
Key milestones in the standardization culminated in the approval of CLOS as part of the ANSI Common Lisp standard. The committee's work continued through the late 1980s and early 1990s, incorporating feedback from public reviews and technical discussions, leading to the release of a draft proposed American National Standard (dpANS) in 1992. This draft was finalized and approved in 1994 as ANSI INCITS 226-1994 (formerly ANSI X3.226-1994), which formally defined CLOS as an integral component of Common Lisp, specifying its core mechanisms for object creation, method dispatch, and slot access. While the ANSI standard provided a precise specification for CLOS's primary features, the Metaobject Protocol (MOP)—which enables customization of class and method behavior—was not fully detailed therein. Instead, the MOP emerged as a de facto standard through the 1991 book The Art of the Metaobject Protocol by Kiczales, des Rivieres, and Bobrow, whose chapters 5 and 6 presented a comprehensive protocol that became the reference for implementers.[57][58]
The standardization process faced significant challenges in reconciling variations among early vendor implementations, which had evolved independently based on informal descriptions in Steele's Common Lisp: The Language (1984) and its second edition (1990). Commercial systems like Lucid Common Lisp and Allegro Common Lisp exhibited differences in CLOS-like features, such as method combination semantics and slot inheritance, stemming from proprietary extensions and incomplete portability of prototypes like PCL. The X3J13 committee addressed these through extensive issue tracking and voting procedures, requiring vendors to align their implementations with the emerging specification to ensure interoperability; this effort was complicated by competitive pressures from languages like C++ and the need for backward compatibility. By the early 1990s, collaboration among vendors, including funding for editorial work, helped resolve these discrepancies, resulting in a standard that balanced innovation with practicality.[56]
Following the 1994 approval, the evolution of CLOS involved minor clarifications and accessibility improvements rather than major revisions. In 1996, Kent Pitman and the Lisp community produced the Common Lisp HyperSpec (CLHS), an online hyperlinked rendition of the ANSI standard that incorporated post-approval cleanup issues and errata, enhancing usability without altering core CLOS definitions. This resource addressed some portability gaps, particularly in the MOP, by documenting implementation-dependent behaviors and encouraging adherence to the AMOP specification; however, full MOP portability remained challenging due to variations in vendor support, prompting later libraries to bridge discrepancies across implementations. The standard has seen no formal updates since, reflecting its stability as a foundation for modern Common Lisp systems.[58]
Implementations and Adaptations
In Common Lisp Implementations
Major Common Lisp implementations, including Steel Bank Common Lisp (SBCL), Clozure Common Lisp (CCL), and Armed Bear Common Lisp (ABCL), provide full compliance with the ANSI Common Lisp standard, which encompasses the core CLOS specification.[59][60] These systems derive their CLOS implementations from the historical Portable Common Loops (PCL) framework, originally developed as a portable reference implementation of CLOS in the late 1980s.[61] PCL served as the foundational base for integrating CLOS into various Lisp environments, enabling early portability before native implementations matured.
The Metaobject Protocol (MOP), an extension to CLOS for metaclasses and introspection, exhibits variations across implementations; for instance, older versions of CMUCL (a predecessor to SBCL) offered only partial MOP support, lacking full conformance to the de facto standard described in The Art of the Metaobject Protocol.[62] To address these discrepancies and promote portability, the Closer to MOP library, released in 2008, provides a compatibility layer that standardizes MOP features across multiple systems, including SBCL, CCL, ABCL, and legacy CMUCL variants.[63][50] This library rectifies absent or incorrect MOP elements, such as generic function invocation protocols, ensuring consistent behavior for advanced CLOS usage like custom method combinations.
Performance in these implementations emphasizes efficient method dispatch, with SBCL optimizing CLOS through compiled discriminating functions that compute effective methods at runtime while leveraging static analysis for speed.[64] Garbage collection impacts CLOS objects similarly to other Lisp data structures, but instance-heavy applications can incur overhead from dynamic slot allocation and class redefinition, potentially leading to fragmentation; SBCL mitigates this via generational GC and precise instance layouts (e.g., 48 bytes base plus 16 bytes per slot pair).[65][66]
Modern extensions like CLOSER-MOP enhance cross-vendor consistency by filling gaps in native MOP support, allowing developers to write portable metaclass code without implementation-specific workarounds, a need highlighted by differences in vendor approaches to features like effective method computation.[50] This addresses historical fragmentation, enabling reliable use of CLOS in diverse environments from native compilers like SBCL to JVM-hosted ABCL.[67]
Ports and Influences in Other Languages
TinyCLOS, developed by Gregor Kiczales in the early 1990s, provided a portable implementation of CLOS concepts in Scheme, enabling the demonstration of the metaobject protocol (MOP) in non-Lisp environments and facilitating its dissemination to other language designers. This Scheme-based port directly influenced the object system in the Dylan programming language, where Kiczales contributed to its design, incorporating CLOS-style multimethods and generic functions for dynamic dispatch. Similarly, TinyCLOS concepts contributed to Julia's multiple dispatch mechanism, which draws from Lisp traditions to support flexible method combinations in a high-performance scientific computing context.
Several languages have adapted CLOS-inspired object systems through specific ports. GOOPS, the object-oriented extension in the Guile Scheme implementation, emulates CLOS's multiple dispatch and generic functions, allowing methods to be defined on combinations of classes and enabling extensible object behavior. In Emacs Lisp, EIEIO (Enhanced Implementation of Emacs Interpreted Objects) provides a CLOS-like framework with classes, methods, and inheritance, supporting dynamic customization for Emacs extensions. The S4 object system in R, introduced for structured data handling, incorporates CLOS principles such as generic functions and method dispatch on argument types, enhancing R's capabilities for statistical modeling and data analysis. For Dylan-like systems, the Common Object System (COS) adapts CLOS multimethods to support sealed classes and dynamic sealing, balancing openness with performance in scripting environments.
CLOS has exerted broader influences on object-oriented features in other languages, particularly in handling multimethods and dynamic behavior. In Python, libraries such as multipledispatch implement CLOS-style multimethods, allowing functions to dispatch based on multiple argument types beyond single inheritance hierarchies. Scala's traits enable mixin-based composition and linearization of methods, echoing CLOS's flexible method combination while integrating with static typing for type-safe concurrency. In contrast to the static typing and compile-time resolution in Java and C++, CLOS's runtime dynamism permits method redefinition and class modification without recompilation, offering greater expressiveness for reflective programming but at the cost of potential performance overhead.
Recent developments include Dynace, a library porting the CLOS MOP to C with C++ bindings, which supports dynamic class creation and method dispatch in performance-critical applications; as of 2024, it remains actively maintained for integrating Lisp-like object capabilities into C/C++ ecosystems.