This odyssey might have seemed a little fast, so let's review what we have seen. The user interface macrodeftclass expands into a function (ensure-class) that makes an instance of standard-class (such in stances are themselves classes) and associates a name with the resulting class. Making an instance of standard-class is part of the metaobject level. Making an instance of standard-class goes through the n ormal initialization protocol, and we saw how the initialization protocol is specialized for making instances of standard-class.
Among the information stored in classes are descriptions of each slot, and these descriptions are used to access slots and create subclasses (by helping create effective slot definitions). We saw that the definition of slot-value uses the information stored in the class to determine where a slot is stored.
Because of properties like this, it is often said that a metaclass determines the representation of instances of its instances, while a class determines the structure (set of slot names) of its instances. Notice that it is the definition of the particulars of the initialization process for instances of standard-class that determines how the ultimate instances are represented. The class defined by the user's defclass stores the set of slots and holds the mapping from slot name to slot representation. Accessors (readers and writers) use slot-value, but I did not show the details, because that involves und erstanding generic functions and methods.
All this flamboyant machinery is irrelevant (except for teaching purposes) unless we find a way to use it to introspect a running CLOS program or to modify or customize CLOS. We will look at an extension to CLOS that can be made by altering the machinery we've seen so far.
But first, I need to point out something about the machinery that I left out. If a defclass form contains an explicit: metaclass class option, the expansion to ensureclass will capture it and pass it on to ensure-class-using-class. If the user does not supply the metaclass, ensure-class-using-class will default the metaclass to standard-class.
The extension we will examine is to provide a metaclass that suppor ts dynamic slots. A dynamic slot is one whose storage is allocated when the slot first becomes bound instead of at instance-creation time . This is useful if an application writer is defining very large classes, but only a small fraction of the slots will be bound in a given instance at any given time.
For example, suppose that, in a simulation, a class is defined with 10,000 slots, but due to the semantics of the simulation every instance has only 10 of the slots bound at a given time. It would be a pity (except to the DRAM manufacturer) to have to have each instance require 40 kbytes when only 40 bytes are needed.
The extension will be used like this by the user:
(defclass c (s1 s2 s3) (sl1 sl2 ... sln) (:metaclass all-dynamic-slots-class))
where sl 1..sln is a long list of slots, each of which is a dynamic.
This problem will be solved by adding an implementational option called dynamic slots that stores certain types of slots in a secondary, sparse data structure. This data structure will be associated with each relevant instance, and accessing dynamic slots will be slower but more space efficient if some dynamic slots are unbound.
This will be accomplished in two steps. The first will be to make a subclass of standard-class called dynamic-slot-class that lets the slot option :allocation :dynamic appear and which will create the secondary storage. The second step will be to make a subclass of dynamic-slot-class called all-dynamic-slots-class that implements the full behavior. For the first step, we will make a subclass of standard-class:
(defclass dynamic-slots-class (standard-class) ())
When we are through, we will be able to write defclasses like this:
(defclass c () ((slot1 :allocation :dynamic) slot2 ) (:metaclass dynamic-slots-class))
The sparse storage facility will be based on a hash table that associates each dynamic-slot instance with an alist of its dynamic slots. Let's look at the hash table functions first (Listing 9). allocate-table-entry simply puts an entry in the table for an instance. read -dynamic-slot-value extracts the alist for an instance and returns the entry in that alist for the supplied slot name, if the entry is present, and signals an error otherwise. write-dynamic-slot-value adds or updates the entry for a slot value of an instance.
(let ((table (make-hash-table :test #'eq))) (defun allocate-table-entry (instance) (setf (gethash instance table) '())) (defun read-dynamic-slot-value (instance slot-name) (let* ((alist (gethash instance table)) (entry (assoc slot-name alist))) (if (null entry) (error "The slot ~S is unbound in the object ~S." slot-name instance) (cdr entry)))) (defun write-dynamic-slot-name (new-value instance slot-name) (let* ((alist (gethash instance table)) (entry (assoc slot-name alist))) (if (null entry) (push `(,slot-name . ,new-value) (gethash instance table)) (setf (cdr entry) new-value)) new-value)))
Now that we know what we're aiming at, let's customize CLOS. Suppose the user defines a class C, which is an instance of dynamic-slots-class; then when the user makes an instance of C, an entry must be placed in the hash table. The code that accomplishes this is:
(defun dynamic-slot-p (slot) (eq (slot-definition-allocation slot) ': dynamic)) (defmethod allocate-instance ((class dynamic-slot-class)) (let ((instance (call-next-method))) (when (some #'dynamic-slot-p (class-slots class)) (allocate-table-entry instance))) instance)
First, the next most specific method for allocate-instance is called to create the basic storage. If there are some dynamic slots, an entry is placed in the hash table. Finally, the instance is returned. The next most specific method is almost certainly the one on standard-class, but it hardly matters since the generic function allocate-instance is specified to return an instance -- notice that we have obey ed this dictum as well. Refer to Listing 4 to see that this method creates storage only for local slots.
Now we can add a method to slot-value-using-class:
(defmethod slot-value-using-class ((class dynamic-slot-class) instance slot-name) (let ((slot (find slot-name (class-slots class) :key #'slot-definition-name))) (if (and slot (dynamic-slot-p slot)) (read-dynamic-slot-value instance slot-name) (call-next-method))))
This method finds the effective slot definition and checks whether it is a dynamic slot; if so, it looks up the value in the hash table, and, if not, it calls the next most specific method, which is the one in Listing 8.
We've left out the methods for slot-boundp and such, but they are similarly defined. Note that a dynamic slot is unbound in an instance if the alist for that instance contains no entry for that slot. Therefore, slot-makunbound must remove the entry from the alist.
Finally, we can take the last step to make a subclass of dynamic-slots-class that renders dynamic every slot:
(defclass all-dynamic-slots-class (dynamic-slots-class) ())
The only thing we have to do is add a method to compute-effective-slot-definition to force the allocation to be dynamic:
(defmethod compute-effective-slot-definition ((class all-dynamic-slots-class) direct-slots) (let ((slot (call-next-method))) (self (slot-definition-allocation slot) ':dynamic) slot))
This method also uses call-next-method to perform the substantial work while only incrementally extending the behavior. This technique is common in object-oriented programming.
In actuality, the Metaobject Protocol is not specified by providing sample code the way I did. The specification lists functions and generic functions, specifies the conditions under which they call each other, lists constraints on user definitions and customizations, and describes invariants that must be maintained.
Courtesy AI Expert