5.5 Subprotocols
Subprotocols
Subprotocols
This section provides an overview of the Metaobject Protocols. The detailed behavior of each function, generic function and macro in the Metaobject Protocol is presented in Chapter 6 . The remainder of this chapter is intended to emphasize connections among the parts of the Metaobject Protocol, and to provide some examples of the kinds of specializations and extensions the protocols are designed to support.
- Metaobject initialization protocols.
- Class finalization protocol.
- Instance structure protocol.
- Funcallable instances.
- Generic function invocation protocol.
- Dependent maintenance protocol.
5.5.1 Metaobject Initialization Protocols
Metaobject initialization protocols
Metaobject initialization protocols
Like other objects, metaobjects can be created by calling make-instance. The initialization arguments passed to make-instance are used to initialize the metaobject in the usual way. The set of legal initialization arguments, and their interpretation, depends on the kind of metaobject being created. Implementations and portable programs are free to extend the set of legal initialization arguments. Detailed information about the initialization of each kind of metaobject are provided in Chapter 6; this section provides an overview and examples of this behavior.
- Initialization of class metaobjects
- Reinitialization of class metaobjects
- Initialization of generic function and method metaobjects
5.5.1.1 Initialization Of Class Metaobjects
Initialization of Class Metaobjects
Initialization of Class Metaobjects
Class metaobjects created with make-instance{#make-instance-1} are usually anonymous; that is, they have no proper name. An anonymous class metaobject can be given a proper name using (setf find-class) and (setf class-name).
When a class metaobject is created with make-instance, it is initialized in the usual way. The initialization arguments passed to make-instance are use to establish the definition of the class. Each initialization argument is checked for errors and associated with the class metaobject. The initialization arguments correspond roughly to the arguments accepted by the defclass macro, and more closely to the arguments accepted by the ensure-class function.
Some class metaobject classes allow their instances to be redefined. When permissible, this is done by calling reinitialize-instance. This is discussed in the next section.
An example of creating an anonymous class directly using make-instance follows:
(flet ((zero () 0)
(propellor () *propellor*))
(make-instance 'standard-class
:name '(my-class foo)
:direct-superclasses (list (find-class 'plane)
another-anonymous-class)
:direct-slots `((:name x
:initform 0
:initfunction ,#'zero
:initargs (:x)
:readers (position-x)
:writers ((setf position-x)))
(:name y
:initform 0
:initfunction ,#'zero
:initargs (:y)
:readers (position-y)
:writers ((setf position-y))))
:direct-default-initargs `((:engine *propellor* ,#'propellor))))
Comments and remarks
This section is named Initialization of Class Metaobjects and appears in Chapter 5 (Concepts) of the original text. There is a section with the same name in Chapter 6 (Generic functions and methods) of the original text. When sections are referred to in the text, it is not specified which one.
5.5.1.2 Reinitialization Of Class Metaobjects
Reinitialization of Class Metaobjects
Reinitialization of Class Metaobjects
Some class metaobject classes allow their instances to be reinitialized. This is done by calling reinitialize-instance. The initialization arguments have the same interpretation as in class initialization.
If the class metaobject was finalized before the call to reinitialize-instance, finalize-inheritance will be called again once all the initialization arguments have been processed and associated with the class metaobject. In addition, once finalization is complete, any dependents of the class metaobject will be updated by calling update-dependent.
5.5.1.3 Initialization Of Generic Function And Method Metaobjects
Initialization of generic function and method metaobjects
Initialization of generic function and method metaobjects
An example of creating a generic function and a method metaobject, and then adding the method to the generic function is shown below. This example is comparable to the method definition shown in this figure.
(let* ((gf (make-instance 'standard-generic-function :lambda-list '(p l &optional visiblyp &key))) (method-class (generic-function-method-class gf))) (multiple-value-bind (lambda initargs) (make-method-lambda gf (class-prototype method-class) '(lambda (p l &optional (visiblyp t) &key color) (set-to-origin p) (when visiblyp (show-move p 0 color))) nil) (add-method gf (apply #'make-instance method-class :function (compile nil lambda) :specializers (list (find-class 'position) (intern-eql-specializer 0)) :qualifiers () :lambda-list '(p l &optional (visiblyp t) &key color) initargs))))
5.5.2 Class Finalization Protocol
Class finalization protocol
Class finalization protocol
Class finalization is the process of computing the information a class inherits from its superclasses and preparing to actually allocate instances of the class. The class finalization process includes computing the class precedence list of the class, the full set of slots accessible in instances of the class and the full set of default initialization arguments for the class. These values are associated with the class metaobject and can be accessed by calling the appropriate reader. In addition, the class finalization process makes decisions about how instances of the class will be implemented.
To support forward-referenced superclasses, and to account for the fact that not all classes are actually instantiated, class finalization is not done as part of the initialization of the class metaobject. Instead, finalization is done as a separate protocol, invoked by calling the generic function finalize-inheritance. The exact point at which finalize-inheritance is called depends on the class of the class metaobject; for standard-class it is called sometime after all the superclasses of the class are defined, but no later than when the first instance of the class is allocated (by allocate-instance).
The first step of class finalization is computing the class precedence list. Doing this first allows subsequent steps to access the class precedence list. This step is performed by calling the generic function compute-class-precedence-list. The value returned from this call is associated with the class metaobject and can be accessed by calling the class-precedence-list generic function.
The second step is computing the full set of slots that will be accessible in instances of the class. This step is performed by calling the generic function compute-slots. The result of this call is a list of effective slot definition metaobjects. This value is associated with the class metaobject and can be accessed by calling the class-slots generic function.
The behavior of compute-slots is itself layered, consisting of calls to effective-slot-definition-class and compute-effective-slot-definition.
The final step of class finalization is computing the full set of initialization arguments for the class. This is done by calling the generic function compute-default-initargs. The value returned by this generic function is associated with the class metaobject and can be accessed by calling class-default-initargs.
If the class was previously finalized, finalize-inheritance may call make-instances-obsolete. The circumstances under which this happens are describe in the section of the CLOS specification called ``Redefining Classes.''.
Forward-referenced classes, which provide a temporary definition for a class which has been referenced but not yet defined, can never be finalized. An error is signalled if finalize-inheritance is called on a forward-referenced class.
5.5.3 Instance Structure Protocol
Instance Structure Protocol
Instance Structure Protocol
The instance structure protocol is responsible for implementing the behavior of the slot access functions like slot-value and (setf slot-value).
For each CLOS slot access function other than slot-exists-p, there is a corresponding generic function which actually provides the behavior of the function. When called, the slot access function finds the pertinent effective slot definition metaobject, calls the corresponding generic function and returns its result. The arguments passed on to the generic function include one additional value, the class of the object argument, which always immediately precedes the object argument.
The correspondences between slot access function and underlying slot access generic function are as follows:
Slot access function Corresponding slot access generic function
slot-boundp slot-boundp-using-class slot-makunbound slot-makunbound-using-class slot-value slot-value-using-class (setf slot-value) (setf slot-value-using-class)
At the lowest level, the instance structure protocol provides only limited mechanisms for portable programs to control the implementation of instances and to directly access the storage associated with instances without going through the indirection of slot access. This is done to allow portable programs to perform certain commonly requested slot access optimizations.
In particular, portable programs can control the implementation of, and obtain direct access to, slots with allocation :instance
and type t. These are called directly accessible slots.
The relevant specified around-method on compute-slots determines the implementation of instances by deciding how each slot in the instance will be stored. For each directly accessible slot, this method allocates a location and associates it with the effective slot definition metaobject. The location can be accessed by calling the slot-definition-location generic function. Locations are non-negative integers. For a given class, the locations increase consecutively, in the order that the directly accessible slots appear in the list of effective slots. (Note that here, the next paragraph, and the specification of this around-method are the only places where the value returned by compute-slots is described as a list rather than a set.)
Given the location of a directly accessible slot, the value of that slot in an instance can be accessed with the appropriate accessor. For standard-class, this accessor is the function standard-instance-access. For funcallable-standard-class, this accessor is the function funcallable-standard-instance-access. In each case, the arguments to the accessor are the instance and the slot location, in that order. See the definition of each accessor for additional restrictions on the use of these function.
Example:
The following example shows the use of this mechanism to implement a new class metaobject class, ordered-class
and class option :slot-order
. This option provides control over the allocation of slot locations. In this simple example implementation, the :slot-order
option is not inherited by subclasses; it controls only instances of the class itself.
(defclass ordered-class (standard-class) ((slot-order :initform () :initarg :slot-order :reader class-slot-order)))
(defmethod compute-slots ((class ordered-class)) (let ((order (class-slot-order class))) (sort (copy-list (call-next-method)) #'(lambda (a b) (< (position (slot-definition-name a) order) (position (slot-definition-name b) order))))))
Following is the source code the user of this extension would write. Note that because the code above doesn't implement inheritance of the :slot-order
option, the function distance
must not be called on instances of subclasses of point
; it can only be called on instances of point
itself.
(defclass point () ((x :initform 0) (y :initform 0)) (:metaclass ordered-class) (:slot-order x y))
(defun distance (point) (sqrt (/ (+ (expt (standard-instance-access point 0) 2) (expt (standard-instance-access point 1) 2)) 2.0)))
In more realistic uses of this mechanism, the calls to the low-level instance structure accessors would not actually appear textually in the source program, but rather would be generated by a meta-level analysis program run during the process of compiling the source program.
5.5.4 Funcallable Instances
Funcallable Instances
Funcallable Instances
Instances of classes which are themselves instances of funcallable-standard-class or one of its subclasses are called funcallable instances. Funcallable instances can only be created by allocate-instance (class funcallable-standard-class).
Like standard instances, funcallable instances have slots with the normal behavior. They differ from standard instances in that they can be used as functions as well; that is, they can be passed to funcall and apply, and they can be stored as the definition of a function name. Associated with each funcallable instance is the function which it runs when it is called. This function can be changed with set-funcallable-instance-function
Example
The following simple example shows the use of funcallable instances to create a simple, defstruct-like facility. (Funcallable instances are useful when a program needs to construct and maintain a set of functions and information about those functions. They make it possible to maintain both as the same object rather than two separate objects linked, for example, by hash tables.)
(defclass constructor ()
((name :initarg :name :accessor constructor-name)
(fields :initarg :fields :accessor constructor-fields))
(:metaclass funcallable-standard-class))
(defmethod initialize-instance :after ((c constructor) &key)
(with-slots (name fields) c
(set-funcallable-instance-function
c
#'(lambda ()
(let ((new (make-array (1+ (length fields)))))
(setf (aref new 0) name)
new)))))
(setq c1 (make-instance 'constructor
:name 'position :fields '(x y)))
#<CONSTRUCTOR 262437>
(setq p1 (funcall c1))
#<ARRAY 3 263674>
5.5.5 Generic Function Invocation Protocol
Generic function invocation protocol
Generic function invocation protocol
Associated with each generic function is its discriminating function. Each time the generic function is called, the discriminating function is called to provide the behavior of the generic function. The discriminating function receives the full set of arguments received by the generic function. It must lookup and execute the appropriate methods, and return the appropriate values.
The discriminating function is computed by the highest layer of the generic function invocation protocol, compute-discriminating-function. Whenever a generic function metaobject is initialized, reinitialized, or a method is added or removed, the discriminating function is recomputed. The new discriminating function is then stored with set-funcallable-instance-function.
Discriminating functions call compute-applicable-methods and compute-applicable-methods-using-classes to compute the methods applicable to the generic functions arguments. Applicable methods are combined by compute-effective-method to produce an effective method. Provisions are made to allow memoization of the method applicability and effective methods computations. (See the description of compute-discriminating-function for details.)
The body of method definitions are processed by make-method-lambda. The result of this generic function is a lambda expression which is processed by either compile or the file compiler to produce a method function. The arguments received by the method function are controlled by the call-method forms appearing in the effective methods. By default, method functions accept two arguments: a list of arguments to the generic function, and a list of next methods. The list of next methods corresponds to the next methods argument to call-method. If call-method appears with additional arguments, these will be passed to the method functions as well; in these cases, make-method-lambda must have created the method lambdas to expect additional arguments.
5.5.6 Dependent Maintenance Protocol
Dependent maintenance protocol
Dependent maintenance protocol
It is convenient for portable metaobjects to be able to memoize information about other metaobjects, portable or otherwise. Because class and generic function metaobjects can be reinitialized, and generic function metaobjects can be modified by adding and removing methods, a means must be provided to update this memoized information.
The dependent maintenance protocol supports this by providing a way to register an object which should be notified whenever a class or generic function is modified. An object which has been registered this way is called a dependent of the class or generic function metaobject. The dependents of class and generic function metaobjects are maintained with add-dependent and remove-dependent. The dependents of a class or generic function metaobject can be accessed with map-dependents. Dependents are notified about a modification by calling update-dependent. (See the specification of update-dependent for detailed description of the circumstances under which it is called.)
To prevent conflicts between two portable programs, or between portable programs and the implementation, portable code must not register metaobjects themselves as dependents. Instead, portable programs which need to record a metaobject as a dependent, should encapsulate that metaobject in some other kind of object, and record that object as the dependent. The results are undefined if this restriction is violated.
Example:
This example shows a general facility for encapsulating metaobjects before recording them as dependents. The facility defines a basic kind of encapsulating object: an updater. Specializations of the basic class can be defined with appropriate special updating behavior. In this way, information about the updating required is associated with each updater rather than with the metaobject being updated.
Updaters are used to encapsulate any metaobject which requires updating when a given class or generic function is modified. The function record-updater is called to both create an updater and add it to the dependents of the class or generic function. Methods on the generic function update-dependent, specialized to the specific class of updater do the appropriate update work.
(defclass updater () ((dependent :initarg :dependent :reader dependent)))
(defun record-updater (class dependee dependent &rest initargs) (let ((updater (apply #'make-instance class :dependent dependent initargs))) (add-dependent dependee updater) updater))
A flush-cache-updater
simply flushes the cache of the dependent when it is updated.
(defclass flush-cache-updater (updater) ())
(defmethod update-dependent (dependee (updater flush-cache-updater) &rest args) (declare (ignore args)) (flush-cache (dependent updater)))