For Development HEAD DRAFTSearch (procedure/syntax/module):

7.5 Metaobject protocol

In CLOS-like object systems, the object system is built on top of itself—that is, things such as the structure of the class, how a class is created, how an instance is created and initialized, and how a method is dispatched and called, are all defined in terms of the object system. For example, a class is just an instance of the class <class> that defines a generic structure and behavior of standard classes. If you subclass <class>, then you can create your own set of classes that behaves differently than the default behavior; in effect, you are creating your own object system.

Metaobject protocols are the definitions of APIs concerning about how the object systems are built—building-block classes, and the names and orders of generic functions to be called during operations of the object system. Subclassing these classes and specializing these methods are the means of customizing object system behaviors.


7.5.1 Class instantiation

Every class is an instance of a group of special classes. A class that can be a class of another class is called metaclass. In Gauche, only the <class> class or its subclasses can be a metaclass.

Expansion of define-class

The define-class macro is basically a wrapper of the code that creates an instance of <class> (or specified metaclass) and bind it to the given name. Suppose you have the following define-class form.

(define-class name (supers)
  slot-specs
  options ...)

It is expanded into a form like this (you can see the exact form by looking at the definition of define-class macro in src/libobj.scm of the source code tree.

(define name
  (let ((tmp1 (make metaclass
                 :name 'name :supers (list supers)
                 :slots (map process-slot-definitions
                             slot-specs)
                 :defined-modules (list (current-module))
                 options ...)))
    ... check class redefinition ...
    ... registering accessor methods ...
    tmp1))

The created class’s class, i.e. metaclass, is determined by the following rules.

  1. If :metaclass option is given to the define-class macro, its value is used. The value must be the <class> class or its descendants.
  2. Otherwise, the metaclasses of the classes in the class precedence list is examined.
    • If all the metaclasses are <class>, then the created class’s metaclass is also <class>.
    • If all the metaclasses are either <class> or another metaclass A, then the created class’ metaclass is A.
    • If the set of metaclasses contains more than one metaclass (A, B, C …) other than <class>, then the created class’ metaclass is a metaclass that inherits all of those metaclasses A, B, C ….

The class’s name, superclasses, and slot definitions are passed as the initialization arguments to the make generic function, with other arguments passed to define-class. The initialization argument defined-modules is passed to remember which module the class is defined, for the redefinition of this class.

The slot specifications slot-specs are processed by internal method process-slot-definitions (which can’t be directly called) to be turned into slot definitions. Specifically, an :init-form slot option is turned into an :init-thunk option, and :getter, :setter and :accessor slot options are quoted.

After the class (an instance of metaclass) is created, the global binding of name is checked. If it is bound to a class, then the class redefinition protocol is invoked (see Class redefinition).

Then, the methods given to :getter, :setter and :accessor slot options in slot-spec are collected and registered to the corresponding generic functions.

Class structure

Class: <class>

The base class of all metaclasses, <class>, has the following slots. Note that these slots are for internal management, and users can’t change those information freely once the class is initialized.

It is recommended to obtain information about a class by procedures described in Class object, instead of directly accessing those slots.

Instance Variable of <class>: name

The name of the class; the symbol given to define-class macro. class-name returns this value.

Instance Variable of <class>: cpl

Class precedence list. class-precedence-list returns this value.

Instance Variable of <class>: direct-supers

The list of direct superclasses. class-direct-supers returns this value.

Instance Variable of <class>: accessors

An assoc list of slot accessors—it encapsulates how each slot should be accessed.

Instance Variable of <class>: slots

A list of slot definitions. class-slots returns this value. See Slot definition object, for the details of slot definitions.

Instance Variable of <class>: direct-slots

A list of slot definitions that is directly specified in this class definition (i.e. not inherited). class-direct-slots returns this value.

Instance Variable of <class>: num-instance-slots

The number of instance allocated slots.

Instance Variable of <class>: direct-subclasses

A list of classes that directly inherits this class. class-direct-subclasses returns this value.

Instance Variable of <class>: direct-methods

A list of methods that has this class in its specializer list. class-direct-methods returns this value.

Instance Variable of <class>: initargs

The initialization argument list when this class is created. The information is used to initialize redefined class (see Class redefinition).

Instance Variable of <class>: defined-modules

A list of modules where this class has a global binding.

Instance Variable of <class>: redefined

If this class has been redefined, this slot contains a reference to the new class. Otherwise, this slot has #f.

Instance Variable of <class>: category

The value of this slot indicates how this class is created. Scheme defined class has a symbol scheme. Other values are for internal use.

The initialize method for <class>

Method: initialize (class <class>) initargs

The define-class macro expands into a call of (make <class> …), which allocates a class metaobject and calls initialize method. This method takes care of computing inheritance order (class precedence list) and calculate slots, and set up various internal slots. Then, at the very end of this method, it freezes the essential class slots; they became immutable.

Calculation of inheritance and slots are handle by generic functions. If you define a metaclass, you can define methods for them to customize how those calculations are done. Class inheritance is calculated by compute-cpl defined below. Slot calculation is a bit involved, and explained in the next subsection (see Customizing slot access).

If your class needs to initialize auxiliary slots, you can define your own initialize method on its metaclass, in which you call next-method first to set up the core part of the <class> structure, then you sets up class-specific part. One caveat is that, after next-method handles initialization of the core <class> part, you can no longer modify essential class slots. If you need to tweak those slots, you can override class-post-initialize method, which is called right before the core class slots are frozen.

Generic function: compute-cpl class

This generic function is called from initialize method on <class>, and responsible to compute the class precedence list (CPL).

At the time this generic function is called, only name and direct-supers slots of class are set. The direct-supers slot contains a list of classes class directly inherits from. All classes in it is already initialized.

It must return a list of classes, starting with class itself and ending with <top>, representing the order of precedence with which methods are searched. The method defined for <class> uses C3 linearlization, which topologically sorts all the classes involved in the inheritance.

Override this method if you need to change how CPL is computed. You might not want to change the actual algorithm unless you emulate different object system, but you can use the method to ensure certain class is always inherited, for example.

Generic function: class-post-initialize class initargs

This generic function is called after all core initialization of class is finished, but before the class is “freezed”, that is, the essential parts of class becomes immutable. If you want to trick object system in some weird way, override this method.

We assume you know what you are doing, for object system assumes the essential parts are computed in the standard way. Messing with them can easily break the system.


7.5.2 Customizing slot access

Generic Function: compute-slots class
Generic Function: compute-get-n-set class slot-definition

These two generic functions are responsible to determine what slots a class has, and how each slot is accessed.

In the initialize method of a class, compute-slots is called after the class’s direct-supers, cpl and direct-slots are set. It must decide what slots the class should have, and what slot options each slot should have, based on those three piece of information. The returned value should have the following form, and it is used as the value of the slots slot of the class.

<slots> : (<slot-definition> ...)
<slot-definition> : (<slot-name> . <slot-options>)
<slot-name> : symbol
<slot-options> : keyword-value alternating list.

After the slots slot of the class is set by the returned value from compute-slots, compute-get-n-set is called for each slot to calculate how to access and modify the slot. The class and the slot definition are the arguments. It must return either one of the followings:

an integer n

This slot becomes n-th instance slot. This is the only way to allocate a slot per instance.

The base method of compute-get-n-set keeps track of the current number of allocated instance slots in the class’s num-instance-slots slot. It is not recommended for other specialized methods to use or change the value of this slot, unless you know a very good reason to override the object system behavior in deep down. Usually it is suffice to call next-method to let the base method reserve an instance slot for you.

See the examples below for modifying instance slot access behaviors.

a list (get-proc set-proc bound?-proc initializable)

The get-proc, set-proc and bound?-proc elements are procedures invoked when this slot of an instance is accessed (either via slot-ref/slot-set!/slot-bound?, or an accessor method specified by :getter/:setter slot options). The value other than get-proc may be #f, and can be omitted if all the values after it is also #f. That is, the simplest form of this type of return value is a list of one element, get-proc.

  • When this slot is about to be read, get-proc is called with an argument, the instance. The returned value of get-proc is the value of the slot.

    The procedure may return #<undef> to indicate the slot is unbound. It triggers the slot-unbound generic function. (That is, this type of slot cannot have #<undef> as its value.)

  • When this slot is about to be written, set-proc is called with two arguments, the instance and the new value. It is called purely for the side effect; the procedure may change the value of other slot of the instance, for example.

    If this element is #f or omitted, the slot becomes read-only; any attempt to write to the slot will raise an error.

  • When slot-bound? is called to check whether the slot of an instance is bound, bound?-proc is called with an argument, the instance. It should return a boolean value which will be the result of slot-bound?.

    If this element is #f or omitted, slot-bound? will call get-proc and returns true if it returns #<undef>.

  • The last element, initializable, is a flag that indicates whether this slot should be initialized when :init-value or :init-form.
A <slot-accessor> object

Access to this slot is redirected through the returned slot-accessor object. See below for more on <slot-accessor>.

The value returned by compute-get-n-set is immediately passed to compute-slot-accessor to create a slot accessor object, which encapsulates how to access and modify the slot.

After all slot definitions are processed by compute-get-n-set and compute-slot-accessor, an assoc list of slot names and <slot-accessor> objects are stored in the class’s accessors slot.

Generic Function: compute-slot-accessor
Method: compute-slot-accessor (class <class>) slot access-specifier

Access-specifier is a value returned from compute-get-n-set. The base method creates an instance of <slot-accessor> that encapsulates how to access the given slot.

Created slot accessor objects are stored (as an assoc list using slot names as keys) in the class’s accessors slot. Standard slot accessors and mutators, such as slot-ref, slot-set!, slot-bound?, and the slot accessor methods specified in :getter, :setter and :accessor slot options, all go through slot accessor object eventually. Specifically, those functions and methods first looks up the slot accessor object of the desired slot, then calls slot-ref-using-accessor etc.

Method: compute-slots (class <class>)

The standard method walks CPL of class and gathers all direct slots. If slots with the same name are found, the one of a class closer to class in CPL takes precedence.

Method: compute-get-n-set (class <class>) slot

The standard processes the slot definition with the following slot allocations: :instance, :class, each-subclass and :virtual.

Function: slot-ref-using-accessor obj slot-accessor
Function: slot-set-using-accessor! obj slot-accessor value
Function: slot-bound-using-accessor? obj slot-accessor
Function: slot-initialize-using-accessor! obj slot-accessor initargs

The low-level slot accessing mechanism. Every function or method that needs to read or write to a slot eventually comes down to one of these functions.

Ordinary programs need not call these functions directly. If you ever need to call them, you have to be careful not to grab the reference to slot-accessor too long; if obj’s class is changed or redefined, slot-accessor can no longer be used.

Here we show a couple of small examples to illustrate how slot access protocol can be customized. You can also look at gauche.mop.* modules (in the source tree, look under lib/gauche/mop/) for more examples.

The first example implements the same functionality of :virtual slot allocation. We add :procedural slot allocation, which adds :ref, :set! and :bound? slot options.

(define-class <procedural-slot-meta> (<class>) ())

(define-method compute-get-n-set ((class <procedural-slot-meta>) slot)
  (if (eqv? (slot-definition-allocation slot) :procedural)
    (let ([get-proc   (slot-definition-option slot :ref)]
          [set-proc   (slot-definition-option slot :set!)]
          [bound-proc (slot-definition-option slot :bound?)])
      (list get-proc set-proc bound-proc))
    (next-method)))

A specialized compute-get-n-set is defined on a metaclass <procedural-slot-meta>. It checks the slot allocation, handles it if it is :procedural, and delegates other slot allocation cases to next-method. This is a typical way to add new slot allocation by layering.

To use this :procedural slot, give <procedural-slot-meta> to a :metaclass argument of define-class:

(define-class <temp> ()
  ((temp-c :init-keyword :temp-c :init-value 0)
   (temp-f :allocation :procedural
           :ref   (lambda (o) (+ (*. (ref o 'temp-c) 9/5) 32))
           :set!  (lambda (o v)
                    (set! (ref o 'temp-c) (*. (- v 32) 5/9)))
           :bound? (lambda (o) (slot-bound? o 'temp-c))))
  :metaclass <procedural-slot-meta>)

An instance of <temp> keeps a temperature in both Celsius and Fahrenheit. Here’s an example interaction.

gosh> (define T (make <temp>))
T
gosh> (d T)
#<<temp> 0xb6b5c0> is an instance of class <temp>
slots:
  temp-c    : 0
  temp-f    : 32.0
gosh> (set! (ref T 'temp-c) 100)
#<undef>
gosh> (d T)
#<<temp> 0xb6b5c0> is an instance of class <temp>
slots:
  temp-c    : 100
  temp-f    : 212.0
gosh> (set! (ref T 'temp-f) 450)
#<undef>
gosh> (d T)
#<<temp> 0xb6b5c0> is an instance of class <temp>
slots:
  temp-c    : 232.22222222222223
  temp-f    : 450.0

Our next example is a simpler version of gauche.mop.validator. We add a slot option :filter, which takes a procedure that is applied to a value to be set to the slot.

(define-class <filter-meta> (<class>) ())

(define-method compute-get-n-set ((class <filter-meta>) slot)
  (cond [(slot-definition-option slot :filter #f)
         => (lambda (f)
              (let1 acc (compute-slot-accessor class slot (next-method))
                (list (lambda (o) (slot-ref-using-accessor o acc))
                      (lambda (o v) (slot-set-using-accessor! o acc (f v)))
                      (lambda (o) (slot-bound-using-accessor? o acc))
                      #t)))]
        [else (next-method)]))

The trick here is to call next-method and compute-slot-accessor to calculate the slot accessor and wrap it. See how this metaclass works:

(define-class <foo> ()
  ((v :init-value 0 :filter x->number))
  :metaclass <filter-meta>)

gosh> (define foo (make <foo>))
foo
gosh> (ref foo'v)
0
gosh> (set! (ref foo'v) "123")
#<undef>
gosh> (ref foo'v)
123

7.5.3 Method instantiation

Method: make (class <method>) :rest initargs

7.5.4 Customizing method application

Generic Function: apply-generic gf args
Generic Function: sort-applicable-methods gf methods args
Generic Function: method-more-specific? method1 method2 classes
Generic Function: apply-methods gf methods args
Generic Function: apply-method gf method build-next args

7.5.5 Customizing class redefinition

Generic Function: class-redefinition old-class new-class

When a class is redefined (see Class redefinition), a new class metaobject is instantiated by (make metaclass initargs …), then this generic function is called with the old class metaobject and new class metaobject. It should transform the information in the old class into the new class.

The default method, (class-redefinition <class> <class>), takes care of updating all the methods referencing to the old class, and propagate changes to the superclasses and subclasses. If you customize this generic function, you should call the default method with next-method to make sure those basic bookkeeping is done, or unexpected things can happen.

Class redefinition mutates lots of structures. If you throw an error in middle of it, the internal state can be left inconsistent.

Internally, the system uses a single mutex dedicated for the class redefition so that only one thread can execute it at a time. You don’t need to worry about other thread stepping on during class-redefinition method (Other thread can still be running for other operations, though, so if you touch objects that can be touched from outside of class redefinition, you should mutex it.)



For Development HEAD DRAFTSearch (procedure/syntax/module):
DRAFT