MetaObject Protocol
This article needs to be checked!. Please help contribute by checking it and making changes in our repository or by clicking on the "Edit this page" link below.
Introduction to the Metaobject Protocol (MOP) in Common Lisp
The Metaobject Protocol (MOP) is a powerful and advanced feature of the Common Lisp Object System (CLOS) that allows you to introspect and customize the behavior of the object system itself. It provides a standardized interface for accessing and modifying the internal workings of classes, methods, generic functions, and other core components of CLOS.
Think of it this way: CLOS defines how objects behave, while the MOP defines how CLOS itself behaves. This meta-level control allows for incredibly flexible and powerful metaprogramming techniques, enabling you to tailor CLOS to specific needs, implement advanced object-oriented features, or even create entirely new object models on top of CLOS.
This tutorial will introduce you to the fundamental concepts of the MOP, including metaobjects, standard metaobject classes, and the key protocols for customizing CLOS behavior.
Key Concepts:
- Metaobjects: Objects that represent the components of CLOS (e.g., classes, methods, generic functions).
- Standard Metaobject Classes: Predefined classes for metaobjects (e.g.,
standard-class
,standard-method
,standard-generic-function
). - Metaobject Protocols: Standardized interfaces for accessing and manipulating metaobjects.
- Introspection: Examining the structure and behavior of CLOS entities.
- Customization: Modifying the default behavior of CLOS.
Table of Contents:
1. Introduction to Metaobjects:
- What are metaobjects?
- The relationship between objects and metaobjects.
- The concept of reification.
2. Standard Metaobject Classes:
standard-class
: The default metaclass for user-defined classes.standard-method
: The default metaclass for methods.standard-generic-function
: The default metaclass for generic functions.- Other important metaobject classes (e.g.,
slot-definition
,method-combination
).
3. Basic MOP Operations:
class-of
: Getting the class of an object.class-name
: Getting the name of a class.find-class
: Finding a class object by name.slot-value
: Accessing slot values (even for metaobjects).
4. Customizing Class Creation:
- Defining custom metaclasses using
defclass
. - Customizing
initialize-instance
for metaclasses. - Adding or modifying slots during class creation.
5. Customizing Method Dispatch:
- Method combinations and their metaobject representation.
- Defining custom method combinations.
- Customizing method selection.
6. Introspection and Reflection:
- Inspecting class structure (slots, superclasses).
- Examining method definitions and specializers.
- Working with generic function metaobjects.
7. Examples and Advanced Techniques:
- Implementing a simple object database.
- Implementing aspects using the MOP.
- Creating a new object model on top of CLOS.
This tutorial will provide clear explanations and practical examples to help you understand the MOP and its capabilities. It will empower you to leverage the full power of CLOS and perform advanced metaprogramming in Common Lisp. It’s important to note that the MOP is an advanced topic and requires a solid understanding of CLOS itself.
1. Introduction to Metaobjects
The Metaobject Protocol (MOP) provides a way to programmatically access and manipulate the structure and behavior of the Common Lisp Object System (CLOS) itself. This is achieved through metaobjects.
1.1 What are Metaobjects?
In standard object-oriented programming, objects are instances of classes. A class defines the structure (slots) and behavior (methods) of its instances. In CLOS, even classes, methods, and generic functions are themselves objects. These objects that represent the components of the object system are called metaobjects.
Think of it this way:
- Objects: Represent data and have behavior.
- Metaobjects: Represent the definitions and behavior of objects, classes, methods and generic functions. They describe how the object system works.
For example:
- A class is a metaobject that describes the structure and behavior of its instances (regular objects).
- A method is a metaobject that describes a specific behavior of a generic function for a particular set of argument specializers.
- A generic function is a metaobject that dispatches method calls based on the classes of the arguments.
1.2 The Relationship Between Objects and Metaobjects
The relationship between objects and metaobjects is a meta-level relationship. An object is an instance of a class. A class is an instance of a metaclass.
Here’s an analogy:
Imagine you have a house (an object). The blueprint for the house is like a class. But who designed the blueprint? An architect. The architect’s design principles and tools are like a metaclass.
In CLOS:
(make-instance 'my-class)
creates an object.defclass
creates a class (which is a metaobject).- The default metaclass (
standard-class
) defines how classes are created and behave.
A class is an instance of a meta-class, therefore we can say that a meta-class is a class whose instances are classes.
1.3 The Concept of Reification
Reification is the process of making something that was previously implicit or abstract explicit and accessible as a data structure. In the context of the MOP, reification means that the components of CLOS (classes, methods, generic functions, etc.) are made available as first-class objects (metaobjects) that can be manipulated programmatically.
Before the MOP, these components were part of the implementation of CLOS and were not directly accessible to the programmer. The MOP reified these components, making them available as objects that can be inspected, modified, and even replaced.
This reification allows for powerful metaprogramming techniques, such as:
- Customizing class creation: You can define custom metaclasses to control how classes are created, adding or modifying slots, changing inheritance behavior, and more.
- Customizing method dispatch: You can define custom method combinations or alter the method selection process.
- Implementing new object models: You can build entirely new object systems on top of CLOS, with different inheritance mechanisms, dispatch strategies, and other features.
Example:
When you define a class using defclass
:
(defclass person ()
((name :initarg :name :accessor person-name)
(age :initarg :age :accessor person-age)))
Behind the scenes, CLOS uses the standard-class
metaclass (by default) to create a class object representing person
. This class object is a metaobject that contains information about the class, such as its name, its superclasses (in this case, standard-object
), its slots, and its methods. The MOP allows you to access and manipulate this class object directly.
In summary, metaobjects are objects that represent the components of CLOS. Reification makes these components accessible to the programmer, enabling powerful metaprogramming. This introduction lays the groundwork for understanding how to use the MOP to customize and extend CLOS. In the next section, we will explore the standard metaobject classes.
2. Standard Metaobject Classes
This section introduces the most important standard metaobject classes in CLOS. These classes represent the core components of the object system and provide the foundation for customization through the MOP.
2.1 standard-class
: The Default Metaclass for User-Defined Classes
standard-class
is the default metaclass for classes defined using defclass
when no explicit metaclass is specified. It provides the standard behavior for class creation, inheritance, and instance creation.
When you define a class like this:
(defclass person ()
((name :initarg :name :accessor person-name)
(age :initarg :age :accessor person-age)))
CLOS creates an instance of standard-class
to represent the person
class. This instance contains information about:
- The class name (
person
). - The superclasses (in this case,
standard-object
by default). - The slots (
name
andage
). - The class precedence list (CPL), which determines the order in which methods are applied.
You can access the class object itself using find-class
:
(find-class 'person) ; Returns the class object for PERSON.
You can check if an object is an instance of standard-class
using typep
:
(typep (find-class 'person) 'standard-class) ; Returns T
2.2 standard-method
: The Default Metaclass for Methods
standard-method
is the default metaclass for methods defined using defmethod
. It represents a specific implementation of a generic function for a particular set of argument specializers.
When you define a method like this:
(defmethod greet ((p person))
(format t "Hello, ~a!~%" (person-name p)))
CLOS creates an instance of standard-method
to represent this method. This instance contains information about:
- The generic function it belongs to (
greet
). - The specializers (in this case,
(person)
). - The method's body (the
format
expression).
You can access the method object through the generic function’s method list using method-combination
and compute-applicable-methods
.
(defgeneric greet (object))
(defmethod greet ((p person))
(format t "Hello, ~a!~%" (person-name p)))
(let ((gf (fdefinition 'greet))) ; get the generic function object
(print (method-combination gf)) ; prints STANDARD
(print (compute-applicable-methods gf (list (make-instance 'person :name "Bob")))) ; prints a list containing the method
)
2.3 standard-generic-function
: The Default Metaclass for Generic Functions
standard-generic-function
is the default metaclass for generic functions defined using defgeneric
. It manages the dispatch of method calls based on the classes of the arguments.
When you define a generic function like this:
(defgeneric greet (object))
CLOS creates an instance of standard-generic-function
. This instance contains information about:
- The generic function's name (
greet
). - The lambda list (the parameter list).
- The method combination (by default,
standard
). - The methods associated with the generic function.
You can access the generic function object using fdefinition
:
(fdefinition 'greet) ; Returns the generic function object for GREET.
You can check if an object is a generic function using fboundp
and functionp
:
(fboundp 'greet) ; Returns T
(functionp (fdefinition 'greet)) ; Returns T
2.4 Other Important Metaobject Classes
Besides the core metaobject classes mentioned above, there are other important metaobject classes that play a role in CLOS:
slot-definition
: Represents a slot in a class. Instances ofstandard-class
have a list ofslot-definition
objects that describe the slots of the class.method-combination
: Represents a method combination type (e.g.,standard
,+
,append
). Method combinations define how methods are combined when multiple methods are applicable to a generic function call.
These metaobject classes are essential for customizing the behavior of CLOS at a deeper level. By understanding their structure and behavior, you can implement advanced metaprogramming techniques.
This section introduced the standard metaobject classes in CLOS. Understanding these classes is crucial for working with the MOP. The next sections will cover how to use the MOP to customize class creation, method dispatch, and other aspects of CLOS.
3. Basic MOP Operations
This section covers some fundamental operations for working with metaobjects in Common Lisp. These operations allow you to introspect and access information about classes, objects, and their components.
3.1 class-of
: Getting the Class of an Object
The class-of
function returns the class of an object. This is a basic but essential MOP operation.
(defclass person ()
((name :initarg :name :accessor person-name)))
(let ((p (make-instance 'person :name "Alice")))
(class-of p)) ; Returns the class object for PERSON.
The result of (class-of p)
is the class object itself, not the symbol 'person
. You can compare it using eq
:
(eq (class-of p) (find-class 'person)) ; Returns T
3.2 class-name
: Getting the Name of a Class
The class-name
function returns the name of a class (as a symbol).
(defclass person () ())
(class-name (find-class 'person)) ; Returns PERSON
If the class is anonymous, the function might return nil
.
3.3 find-class
: Finding a Class Object by Name
The find-class
function takes a symbol representing a class name and returns the corresponding class object.
(defclass person () ())
(find-class 'person) ; Returns the class object for PERSON.
If the class is not found, find-class
returns nil
. You can specify a second optional argument which is a boolean value indicating whether an error should be signaled if the class is not found. The default is t
.
(find-class 'non-existent-class) ; Signals a simple error
(find-class 'non-existent-class nil) ; Returns nil
find-class
searches the current class environment, which is influenced by packages. By default, it searches the current package.
3.4 slot-value
: Accessing Slot Values (Even for Metaobjects)
The slot-value
function is used to access the value of a slot in an object. Importantly, it can also be used to access slots of metaobjects. This is a key aspect of the MOP, as it allows you to inspect and manipulate the internal state of classes, methods, and generic functions.
(defclass person ()
((name :initarg :name :accessor person-name)))
(let ((p (make-instance 'person :name "Bob")))
(slot-value p 'name)) ; Returns "Bob"
; Accessing slots of a metaobject (class):
(let ((person-class (find-class 'person)))
(slot-value person-class 'name)) ; This would signal an error, because the class object does not have a slot named name
(defclass my-class ()
((my-slot :initform 42)))
(let ((my-class-object (find-class 'my-class)))
(print (slot-value my-class-object 'slots)) ; prints a list of slot definitions
(let ((my-slot-definition (first (slot-value my-class-object 'slots))))
(print (slot-value my-slot-definition 'name)) ; prints MY-SLOT
(print (slot-value my-slot-definition 'initform)) ; prints 42
)
)
In the last example, we accessed the slots
slot of the my-class
class object. This slot contains a list of slot-definition
metaobjects, which themselves have slots like name
and initform
. This demonstrates how slot-value
can be used to navigate the metaobject hierarchy.
It’s important to note that accessing slots of metaobjects directly using slot-value
can be implementation-dependent. The MOP provides a more portable and standardized way to access metaobject information through metaobject protocols (generic functions defined on metaobject classes), which we will cover in later sections. However, slot-value
remains a useful tool for basic introspection and is often used in MOP implementations.
These basic MOP operations (class-of
, class-name
, find-class
, slot-value
) provide the foundation for more advanced metaprogramming techniques. They allow you to inspect and understand the structure of your CLOS objects and classes. The following sections will build upon these basics to show you how to customize class creation and method dispatch.
4. Customizing Class Creation
This section explains how to customize the process of class creation in CLOS using the Metaobject Protocol. This involves defining custom metaclasses and customizing the initialize-instance
method for these metaclasses.
4.1 Defining Custom Metaclasses using defclass
A metaclass is a class whose instances are classes. You define a custom metaclass using defclass
, just like defining any other class, but you typically inherit from standard-class
(or another appropriate metaclass).
(defclass my-metaclass (standard-class)
((extra-info :initarg :extra-info :accessor extra-info)))
This defines a metaclass called my-metaclass
with an additional slot called extra-info
.
To use a custom metaclass for a class, you specify the :metaclass
option in the defclass
form:
(defclass my-class ()
((a :initarg :a :accessor my-class-a))
(:metaclass my-metaclass)
(:extra-info "Some metadata"))
Now, the my-class
class is an instance of my-metaclass
, not standard-class
. You can access the extra-info
slot of the my-class
class object:
(slot-value (find-class 'my-class) 'extra-info) ; Returns "Some metadata"
4.2 Customizing initialize-instance
for Metaclasses
The initialize-instance
method is called when a new instance of a class is created (including when a new class is created, since classes are instances of metaclasses). By specializing initialize-instance
on your custom metaclass, you can customize the class creation process.
(defclass my-metaclass (standard-class)
((creation-time :accessor creation-time)))
(defmethod initialize-instance ((class my-metaclass) &rest initargs)
(call-next-method) ; Call the standard initialize-instance method
(setf (creation-time class) (get-universal-time))
class)
(defclass my-class ()
()
(:metaclass my-metaclass))
(creation-time (find-class 'my-class)) ; Returns the time when my-class was created.
In this example, we specialize initialize-instance
on my-metaclass
. We first call call-next-method
to ensure that the standard initialization process is performed. Then, we set the creation-time
slot of the class object.
You can use &key
parameters in the initialize-instance
method to handle custom options passed to defclass
:
(defclass my-metaclass (standard-class)
((custom-option :initarg :custom-option :accessor custom-option)))
(defmethod initialize-instance ((class my-metaclass) &rest initargs &key custom-option &allow-other-keys)
(call-next-method)
(when custom-option
(setf (custom-option class) custom-option))
class)
(defclass my-class ()
()
(:metaclass my-metaclass)
(:custom-option "A custom value"))
(custom-option (find-class 'my-class)) ; Returns "A custom value"
4.3 Adding or Modifying Slots During Class Creation
You can add or modify slots during class creation by manipulating the slots
slot of the class metaobject within initialize-instance
.
(defclass my-metaclass (standard-class) ())
(defmethod initialize-instance ((class my-metaclass) &rest initargs)
(call-next-method)
; Add a new slot to the class
(setf (slot-value class 'slots)
(append (slot-value class 'slots)
(list (make-instance 'standard-effective-slot-definition
:name 'added-slot
:initform 0))))
class)
(defclass my-class ()
((original-slot :initform "foo"))
(:metaclass my-metaclass))
(mapcar #'slot-definition-name (slot-value (find-class 'my-class) 'slots)) ; prints (ORIGINAL-SLOT ADDED-SLOT)
In this example, we add a new slot named added-slot
with an initform
of 0 to the class being created. standard-effective-slot-definition
is used to create the new slot definition.
Important Considerations:
- Modifying the structure of a class after instances have been created can have complex consequences. CLOS provides mechanisms for updating instances, but it's often best to design your classes carefully from the beginning.
- The MOP is a powerful tool, but it should be used judiciously. Overuse of metaprogramming can make code harder to understand and maintain.
This section covered how to customize class creation using custom metaclasses and the initialize-instance
method. This allows you to exert fine-grained control over the structure and behavior of your classes. The next sections will cover customizing method dispatch and other advanced MOP topics.
5. Customizing Method Dispatch
This section delves into customizing method dispatch in CLOS using the Metaobject Protocol. This primarily involves understanding and manipulating method combinations.
5.1 Method Combinations and Their Metaobject Representation
When a generic function is called, CLOS determines which methods are applicable (their specializers match the arguments). If multiple methods are applicable, CLOS uses a method combination to determine how the results of these methods are combined.
The standard method combination is called standard
. It defines the order in which methods are called (most specific to least specific) and how their results are combined (primary methods are called first, then before
methods, then after
methods, and finally around
methods).
Method combinations are themselves represented by metaobjects of class method-combination
. You can access the method combination of a generic function using method-combination
:
(defgeneric my-generic-function (x))
(method-combination (fdefinition 'my-generic-function)) ; Returns STANDARD
5.2 Defining Custom Method Combinations
You can define custom method combinations using define-method-combination
. The syntax is complex and allows for a high degree of customization. Here, we'll cover a simplified example.
(define-method-combination my-combination (&optional (order :most-specific-first))
((methods (method-combination-methods)))
(case order
(:most-specific-first
`(progn ,@(mapcar #'(lambda (method) `(call-method ,method)) methods)))
(:most-specific-last
`(progn ,@(reverse (mapcar #'(lambda (method) `(call-method ,method)) methods))))))
This defines a method combination called my-combination
that takes an optional argument order
. If order
is :most-specific-first
(the default), it calls the applicable methods from most specific to least specific. If order
is :most-specific-last
, it calls them in reverse order.
To use a custom method combination, you specify the :method-combination
option in defgeneric
:
(defgeneric my-generic-function (x)
(:method-combination my-combination))
(defmethod my-generic-function ((x number))
(format t "Number method~%")
x)
(defmethod my-generic-function ((x integer))
(format t "Integer method~%")
(* x 2))
(my-generic-function 5)
; Output:
; Integer method
; Number method
; 10
(defgeneric my-other-generic-function (x)
(:method-combination my-combination (:most-specific-last)))
(defmethod my-other-generic-function ((x number))
(format t "Number method~%")
x)
(defmethod my-other-generic-function ((x integer))
(format t "Integer method~%")
(* x 2))
(my-other-generic-function 5)
; Output:
; Number method
; Integer method
; 10
5.3 Customizing Method Selection
You can customize method selection by specializing the generic functions that are responsible for determining applicable methods and ordering them. The most important generic function for this is compute-applicable-methods
.
compute-applicable-methods
takes a generic function and a list of arguments and returns a list of applicable methods, ordered according to the method combination. By specializing this generic function, you can change the method selection process entirely.
Here's a simplified example that demonstrates how to filter applicable methods based on a condition:
(defgeneric my-generic-function (x))
(defmethod compute-applicable-methods ((gf standard-generic-function) args)
(remove-if-not #'(lambda (method)
(let ((specializer (first (method-specializers method))))
(or (eq specializer 't) ; Always include methods specialized on T
(evenp (first args))))) ; Only include methods if the first argument is even
(call-next-method))) ; Call the standard compute-applicable-methods
(defmethod my-generic-function ((x number))
(format t "Number method~%")
x)
(defmethod my-generic-function ((x integer))
(format t "Integer method~%")
(* x 2))
(my-generic-function 4)
; Output:
; Integer method
; 8
(my-generic-function 5)
; Output:
; Number method
; 5
In this example, we specialize compute-applicable-methods
to filter the applicable methods, only keeping those where the first argument is even (or if the method is specialized on T
).
Customizing method dispatch is an advanced technique that allows for very fine-grained control over the behavior of CLOS. However, it's often complex and should be used only when necessary. Most of the time, the standard method combination and the standard method selection process are sufficient.
This section covered customizing method dispatch using custom method combinations and specializing compute-applicable-methods
. This enables very powerful and flexible control over how generic functions behave. This concludes the tutorial on the Metaobject Protocol.
6. Introspection and Reflection in CLOS
Introspection and reflection are powerful features of CLOS that allow you to examine and manipulate the structure and behavior of objects, classes, methods, and generic functions at runtime. This section explores how to perform these operations using the Metaobject Protocol.
6.1 Inspecting Class Structure (Slots, Superclasses)
You can inspect the structure of a class using several MOP functions:
-
class-slots
: Returns a list ofslot-definition
metaobjects for a given class. Eachslot-definition
contains information about a slot, such as its name, initargs, initform, and accessors.(defclass person ()
((name :initarg :name :accessor person-name)
(age :initarg :age :accessor person-age)))
(mapcar #'slot-definition-name (class-slots (find-class 'person))) ; Returns (NAME AGE)
(mapcar #'slot-definition-initargs (class-slots (find-class 'person))) ; Returns ((:NAME) (:AGE)) -
class-direct-slots
: Returns a list ofslot-definition
metaobjects for the slots directly defined in the class (excluding inherited slots).(defclass employee (person)
((employee-id :initarg :id :accessor employee-id)))
(mapcar #'slot-definition-name (class-slots (find-class 'employee))) ; Returns (NAME AGE EMPLOYEE-ID)
(mapcar #'slot-definition-name (class-direct-slots (find-class 'employee))) ; Returns (EMPLOYEE-ID) -
class-precedence-list
: Returns the class precedence list (CPL) for a class. The CPL determines the order in which methods are applied when a generic function is called.(class-precedence-list (find-class 'employee)) ; Returns a list of classes: (EMPLOYEE PERSON STANDARD-OBJECT T)
-
class-direct-superclasses
: Returns the direct superclasses of a class.(class-direct-superclasses (find-class 'employee)) ; Returns (PERSON)
6.2 Examining Method Definitions and Specializers
You can examine method definitions and specializers using the following functions:
-
method-specializers
: Returns a list of the specializers of a method.(defgeneric greet (x))
(defmethod greet ((x person))
(format t "Hello, ~a!~%" (person-name x)))
(let ((method (find-method #'greet '() (list (find-class 'person)))))
(method-specializers method)) ; Returns (PERSON) -
method-lambda-list
: Returns the lambda list of a method.(let ((method (find-method #'greet '() (list (find-class 'person)))))
(method-lambda-list method)) ; Returns (X) -
method-function
: Returns the compiled function that implements the method.(let ((method (find-method #'greet '() (list (find-class 'person)))))
(function-lambda-expression (method-function method))) ; returns the lambda expression of the method -
find-method
: Finds a method of a generic function that matches a given set of specializers.(find-method #'greet '() (list (find-class 'person))) ; Returns the method object.
(find-method #'greet '() (list (find-class 'integer))) ; Returns nil because there is no method specialized on integer
6.3 Working with Generic Function Metaobjects
You can access and manipulate generic function metaobjects using these functions:
-
generic-function-name
: Returns the name of a generic function.(defgeneric greet (x))
(generic-function-name (fdefinition 'greet)) ; Returns GREET -
generic-function-methods
: Returns a list of all methods associated with a generic function.(defmethod greet ((x number)) nil)
(defmethod greet ((x integer)) nil)
(length (generic-function-methods (fdefinition 'greet))) ; Returns 2 -
generic-function-method-class
: Returns the class of the methods associated with a generic function (usuallystandard-method
).(generic-function-method-class (fdefinition 'greet)) ; Returns STANDARD-METHOD
-
method-combination
: Returns the method combination object of the generic function.(method-combination (fdefinition 'greet)) ; Returns STANDARD
Example: Printing Information about a Class:
This example demonstrates how to use introspection to print information about a class:
(defun print-class-info (class-name)
(let ((class (find-class class-name)))
(when class
(format t "Class: ~a~%" (class-name class))
(format t "Superclasses: ~a~%" (class-direct-superclasses class))
(format t "Slots:~%")
(dolist (slot (class-slots class))
(format t " ~a (initargs: ~a)~%"
(slot-definition-name slot)
(slot-definition-initargs slot))))))
(print-class-info 'employee)
This function prints the name, superclasses, and slots of a given class.
Introspection and reflection are powerful tools for understanding and manipulating the structure and behavior of your CLOS code. They are particularly useful for debugging, metaprogramming, and building tools that analyze or manipulate code. This concludes the tutorial on the Metaobject Protocol. While many more advanced features exist, this introduction should provide a solid foundation for further exploration.
7. Examples and Advanced Techniques using the MOP
This section explores some more advanced techniques and provides examples of how the Metaobject Protocol (MOP) can be used to solve real-world problems.
7.1 Implementing a Simple Object Database
The MOP can be used to implement a simple in-memory object database. This example demonstrates how to automatically assign unique IDs to objects upon creation.
(defclass db-class (standard-class)
((next-id :initform 0 :accessor next-id)))
(defmethod initialize-instance ((class db-class) &rest initargs)
(call-next-method)
class)
(defmethod make-instance ((class db-class) &rest initargs)
(let ((instance (call-next-method)))
(setf (getf initargs :id) (incf (next-id class))) ; Auto-assign ID
(apply #'reinitialize-instance instance initargs)
instance))
(defclass db-object ()
((id :initarg :id :accessor object-id))
(:metaclass db-class))
(defclass person (db-object)
((name :initarg :name :accessor person-name)))
(let ((p1 (make-instance 'person :name "Alice"))
(p2 (make-instance 'person :name "Bob")))
(format t "P1 ID: ~a, Name: ~a~%" (object-id p1) (person-name p1))
(format t "P2 ID: ~a, Name: ~a~%" (object-id p2) (person-name p2)))
; Output:
; P1 ID: 1, Name: Alice
; P2 ID: 2, Name: Bob
Here's how it works:
db-class
is a custom metaclass that inherits fromstandard-class
and has a slotnext-id
to keep track of the next available ID.- The
initialize-instance
method fordb-class
does nothing special but is needed to make thenext-id
slot work. - The
make-instance
method specialized ondb-class
now automatically assigns a unique ID to each instance by incrementingnext-id
. db-object
is a base class for all database objects, which has anid
slot.person
inherits fromdb-object
and thus automatically gains the ID functionality.
This is a simple example, but it illustrates how the MOP can be used to add common behavior to classes automatically.
7.2 Implementing Aspects Using the MOP
Aspect-Oriented Programming (AOP) is a programming paradigm that allows you to modularize cross-cutting concerns (aspects) such as logging, tracing, or security. The MOP can be used to implement aspects in CLOS.
This example demonstrates how to implement a simple tracing aspect:
(defclass traced-class (standard-class) ())
(defmethod initialize-instance ((class traced-class) &rest initargs)
(call-next-method)
(dolist (method (generic-function-methods (fdefinition (intern (format nil "~a-~a" (package-name (symbol-package (class-name class))) (class-name class))))))
(let ((original-function (method-function method)))
(setf (method-function method)
#'(lambda (&rest args)
(format t "Entering ~a with args: ~a~%" method args)
(multiple-value-prog1 (apply original-function args)
(format t "Exiting ~a~%" method))))))
class)
(defgeneric person-greet (p))
(defclass person ()
((name :initarg :name :accessor person-name))
(:metaclass traced-class))
(defmethod person-greet ((p person))
(format t "Hello ~a~%" (person-name p)))
(person-greet (make-instance 'person :name "Alice"))
; Output:
; Entering #<STANDARD-METHOD PERSON-GREET (PERSON)> with args: (#<PERSON {1004838383}>)
; Hello Alice
; Exiting #<STANDARD-METHOD PERSON-GREET (PERSON)>
Here's how it works:
traced-class
is a custom metaclass.- The
initialize-instance
method fortraced-class
iterates through all methods associated with the class, and wraps the original method function with a new function that prints tracing information before and after calling the original method. - By making a class use the
traced-class
metaclass, all methods of that class are automatically traced.
This is a simplified example of aspect implementation. More complex aspects can be implemented using more advanced MOP techniques.
7.3 Creating a New Object Model on Top of CLOS
The MOP provides enough power to implement entirely new object models on top of CLOS. This is a very advanced technique and is rarely necessary for most applications. However, it demonstrates the extreme flexibility of the MOP.
Implementing a new object model typically involves:
- Defining custom metaclasses for classes, methods, and generic functions.
- Customizing method dispatch.
- Defining new method combinations.
This is a very complex topic and is beyond the scope of this introductory tutorial. However, it's important to understand that the MOP provides this level of customization.
These examples illustrate some of the advanced capabilities of the MOP. It's a powerful tool that allows you to customize and extend CLOS in significant ways. However, it's important to use it judiciously, as overuse can make code more complex. For most applications, the standard behavior of CLOS is sufficient.
The standard book on the subject is The Art of the MetaObject Protocol. We have some of the chapters which are allowed to be reprinted in the Technical Reference, however for an in depth explanation of CL MOP, this book is the ultimate reference.