Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Parallel

Lisp: Classes in the Metaobject Protocol


Just Like CLOS

It would be reasonable for an innovative car designer to take the user interface of a car -- the control layout and its functional specification -- and design a different connection and metaobject layer, which would be functionally equivalent to a car but very different underneath. Turbine cars and hovercraft are simple examples.

A lot of CLOS programs will use only the user interface layer, some times dipping into the connection layer. But occasionally the very nature of CLOS needs to be customized, and the metaobject layer must be used. CLOS, along with the Metaobject Protocol, is defined in such a way that implementors are somewhat constrained by having not only the user interface layer but parts of the connection and metaobject layers specified.

The full Metaobject Protocol describes how classes are made and represented, slots are represented, inheritance is implemented, generic functions and methods are made and represented, generic functions invoke methods, how method combinations are represented and used.

To start, we will look at the representation of class objects and get far enough to define customized slots. In the course of our examination of this part of the Metaobject Protocol, we will look at how classes are created and how their slots are accessed. My explanation will be in terms of hypothetical code that creates classes and accesses slots at the accessor level. My code definitions are inaccurate and intended only to give a sense of their actions. For example, for simplicity I will assume that when a class is defined, all of its superclasses are already defined.

Objects that represent classes should provide the following information: the class's name (if any), its direct superclasses, the slots it directly defines, its class precedence list (though this can be computed), the complete set of slots (including inherited ones), its direct subclasses, and the methods defined directly on this class.

Listing 1 shows the information for the rectangle class object (previously defined). With such information, classes can be found by name, inheritance can be determined, instances can be made, and methods can be selected.

Name:                  rectangle
Direct superclasses:   none
Direct slots:          (height weight)
Class precedence list: (rectangle standard-object t)
Effective slots:       (height weight)
Direct subclasses:     none
Direct methods:        none
Listing 1: Rectangle class object.

Because class objects are objects, they are instances of classes. Therefore, the class rectangle is itself an instance of a class, called standard-class. Listing 2 shows the definition of standard-class as it would be specified by a defclass form.

(defclass standard-class ()
  ((name                        :initarg :name
                               :accessor class-name)
   (direct-superclasses   :initarg :direct-superclasses)
   (direct-slots         :accessor class-direct-slots)
   (class-precedence-list :accessor class-precedence-list)
   (effective-slots     :accessor class-slots)
   (direct-subclasses     :initform '() 
                              :accessor class-direct-superclasses)
   (direct-methods        :initform '() 
                            :accessor class-direct-methods)))
Listing 2: Definition of standard-class.

If user-defined classes are instances of standard-class, somehow defclass (the user interface macro that defines classes) must eventually run make-instance. To do this, defclass expands into a function that calls a generic function in the metaobject layer.

make-instance invokes the initialization protocol. The initialization protocol lets users specify how to make instances of the classes they defined; therefore it is logical that the initialization protocol would be used to specify how to make classes. This is exactly how classes are made.

By looking at how instances are made, we will understand most of what we should know about class and instance representation. Also, by looking at the expansion of defclass into functions that use metaobje cts, we will see a slice through the three layers and better understa nd their relationship.

The code shows how defclass (user interface layer) expands into a call to an underlying function called ensure-class (connection layer):

(defmacro defclass
    (name direct-superclasses
    direct-slots rest options)
    '(ensure-class ',name
         :direct-superclasses
          ,(canonicalize-direct-superclasses
             direct-superclasses)
         :direct-slots
         ,(canonicalize-direct-slots
             direct-slots)
         :name ',name
         ,@(canonicalize-defclass-options
              options)))

The functions that start with canonicalize- perform the bulk of parsing work by putting the superclasses, slots, and options into a standard format. defclass has two important functions: it creates a class object and it associates that class with a name. canonicalize-direct-superclasses dereferences the class names into class objects, and each slot description is turned into a property list that contains the name of the slot, its initform and initfunction, its accessors separated into readers and writers, and its coalesced initargs.

The function ensure-class calls the generic function ensure-class-using-class (metaobject layer), which takes as arguments the class (or nil), name, and keyword arguments, including the ones we've seen for ensure-class, plus the metaclass and a few other things as keyword arguments. Basically, this generic function calls make-instance on the metaclass and with the proper initialization arguments (Listing 3).

(defmethod ensure-class-using-class
  ((class-object standard-class)
    class-name 
   rest all-keys 
   key direct-default-initargs
        direct-slots
        direct-superclasses
        name
        (metaclass (find-class 'standard-class))
   allow-other-keys)
  (let ((initialization-arguments ...))
    (cond ((null class)
           (let ((class
                   (apply #'make-instance 
                     metaclass
                     initialization-arguments)))
             (setf (find-class class-name) class)
             class)))
    ...))
Listing 3: Generic function calling make-instance.

We will often see the idiom exemplified by ensure-class calling ensure-class-using-class, and it is important to understand the reason for it. In the idiom, the basic function is specified to take some object and perform an operation on it. But that operation really depen ds on the metaclass of the object and not on the object itself, and so the function calls a generic function that takes an additional argu ment, which is the class of the object. An easier example to understand is slot-value, which takes an object and a slot name and returns the value stored in that slot. But to accomplish this, slot-value must access the representation of instances. As we'll see in tracing how classes are made, the representation of instances is determined by metaclasses, so the real functionality of accessing a slot must cons ult the metaclass. This is done by defining slot-value to invoke slot-value-using-class:

 (defun slot-value
         (instance name)
    (slot-value-using-class
        (class-of instance) 
        instance 
        name))

Before calling make-instance, ensure-class-using-class computes the initialization arguments from the keyword arguments supplied. make-instance-instance is called with the metaclass as its first argument . That is, an instance is made of the metaclass passed in as the metaclass keyword argument; if no such metaclass is passed in, standard-class is used. Notice that two names are passed in: the required argument classname is used to set the class that find-class returns, and the keyword argument name is passed into make-instance to set the class name. This defines a proper name of the class -- a symbol s is a proper name for a class c if (class-name c) = s and find-class s)=c.

Recall that make-instance is defined as if by the code:


(defmethod make-instance
    ((class standard-class) 
        &rest initargs) 
    (setq initargs (default-initargs class initargs))
     ... 
   (let ((instance
         (apply #'allocate-instance
                 class initargs))) 
   (apply #'initialize-instance
         instance initargs) instance))

To finish the story of building a class, we need to look at what al locate-instance and initialize-instance do. Listing 4 shows what allocate-instance could be like.

(defparameter secret-unbound-value (list "slot unbound"))
 
(defun allocate-slot-storage (size initial-value)
  (make-array size :initial-element initial-value))
 
(defun instance-slot-p (slot)
  (eq (slot-definition-allocation slot) ':instance))

(defmethod allocate-instance
   ((class standard-class) rest initargs)
  (declare (ignore initargs)) 
  (allocate-std-instance
    class
    (allocate-slot-storage 
      (count-if #'instance-slot-p (class-slots class))
      secret-unbound-value)))
Listing 4: What allocate instance looks like.

allocate-instance allocates the storage used to hold an instance. That is, this function determines how the storage for an instance is carved out of memory. Notice that allocate-instance is a generic function that depends on metaclasses.

allocate-instance calls allocate-std-instance, which builds a defstruct-like structure with two structure slots: one slot contains the class and the other contains a vector to hold the instance slots. In this code, I have assumed that all slots are local slots, which simplifies the definition a little without loss of generality.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.