For Gauche 0.9.5


Previous: , Up: オブジェクトシステム   [Contents][Index]

7.5 メタオブジェクトプロトコル

CLOS 風のオブジェクトシステムでは、オブジェクトシステムがそれ自身の上に 構築されます。すなわち、クラス構造のようなもの、クラスをどのように 生成するか、インスタンスをどのように生成し初期化するか、メソッドをどのように ディスパッチし呼び出すか、これらはすべてオブジェクトシステムによって、 定義されます。たとえば、クラスはジェネリックな構造と標準的クラスの 振舞いを定義する <class> クラスのインスタンスです。<class> をサブクラス化すると、デフォルトのものとは違う振舞いをする、独自の クラス集合をつくることができます。これは結局、独自のオブジェクトシステムを つくることになります。

メタオブジェクトプロトコルは、どのようにオブジェクトシステムを 構築するかに関連する API 群の定義です。ブロック構築のクラス、オブジェクト システムを操作するあいだに呼ばれるジェネリック関数の名前と順序などです。 これらのクラスをサブクラス化し、これらのメソッドを特定化することは、 オブジェクトシステムの振舞いをカスタマイズすることを意味します。


Next: , Previous: , Up: メタオブジェクトプロトコル   [Contents][Index]

7.5.1 クラスのインスタンシエーション

すべてのクラスはある特殊なクラスのグループのインスタンスになっています。 他のクラスのクラスになれるようなクラスのことを メタクラス と呼びます。 Gauche では <class> クラスおよびそのサブクラスのみがメタクラスに なれます。

define-class の展開

define-class マクロは基本的には <class> (あるいは指定された メタクラス)のインスタンスを生成するコードのラッパーで、それを与えられた 名前に束縛します。以下のような define-class 形式を前提とします。

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

これを次のように展開します。 (完全な展開形を知りたければ、ソースツリーのsrc/libobj.scm にあるdefault-classの定義を見てください。)

(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))

生成されるクラスのクラス、つまり、metaclass は以下のルールで 決定されます。

  1. もし、:metaclass オプションが define-class マクロに 与えられていれば、その値を使います。その値は、<class> クラスか あるいはその子孫でなければなりません。
  2. さもなければ、クラス順位リスト中のクラスのメタクラスが試されます。

クラスの名前、スーパークラス、スロットの定義は初期化引数として ジェネリック関数 make に引き渡されます。また、 define-class のスロット定義以降に渡されたキーワード-値リストも 追加の初期化引数として make に渡されます。 初期化引数 define-modules は どのモジュールでそのクラスが定義されたかを覚えておくためにのものです。 これはこのクラスの再定義の時に使われます。

スロットの仕様 slot-specs は内部メソッド process-slot-definitions (これは直接呼び出すことはできません)で処理され、スロット定義になります。 厳密には、:init-form スロットオプションは、:init-thunk オプション になり、:getter:setter:accessor のスロットオプションは 引用されます。

クラス(metaclass のインスタンス)が生成された後、name のグローバル な束縛がチェックされます。それが、クラスに束縛されていれば、クラスの再定義 プロトコルが起動されます(クラスの再定義 参照)。

その後、slot-specs 中で、:getter:setter:accessor スロットオプションに与えられたメソッドが集められ、対応する ジェネリック関数に登録されます。

クラス構造

Class: <class>

すべてのメタクラスのベースクラスである <class> は以下のような スロットを持っています。これらのスロットは内部的な管理のためにあるので クラスが初期化された後に、これらの値を自由に変更することはできません。

クラスの情報を得るには、これらのスロットに直接 アクセスするのではなく、クラスオブジェクト にある手続きを使うことをおすすめします。

Instance Variable of <class>: name

クラスの名前、define-class マクロに与えられたシンボルです。 class-name はこの値を返します。

Instance Variable of <class>: cpl

クラス順位リストです。class-precedence-list はこの値を返します。

Instance Variable of <class>: direct-supers

直接スーパークラスのリストです。 class-direct-supers はこの値を返します。

Instance Variable of <class>: accessors

スロットアクセサの連想リストです。これは各スロットがどのようにアクセスされる べきかをカプセル化しています。

Instance Variable of <class>: slots

スロット定義のリストです。class-slots はこの値を返します。 スロット定義についての詳細は、スロット定義オブジェクト を参照してください。

Instance Variable of <class>: direct-slots

このクラスの定義で直接指定された(つまり継承したものではない)スロット定義の リストです。class-direct-slots はこの値を返します。

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

インスタンスにアロケートされるスロットの数です。

Instance Variable of <class>: direct-subclasses

このクラスを直接継承しているクラスのリストです。 class-direct-subclasses はこの値を返します。

Instance Variable of <class>: direct-methods

このクラスを特定化子リスト中にもつメソッドのリストです。 class-direct-methods はこの値を返します。

Instance Variable of <class>: initargs

このクラスが生成されるときの初期化引数リストです。この情報は 再定義されたクラスを初期化するのに使います(クラスの再定義 参照)。

Instance Variable of <class>: defined-modules

このクラスがグローバル束縛をもつモジュールのリストです。

Instance Variable of <class>: redefined

このクラスが再定義された場合、このスロットは新しいクラスへの参照を含みます。 そうでない場合にはこのスロットは #f をもっています。

Instance Variable of <class>: category

このスロットの値は、このクラスがどのように生成されたかを示しています。 Scheme 定義のクラスは、scheme というシンボルを持っています。それ以外の 値は内部的に使用するだけです。

<class> 用の initialize メソッド

Method: initialize (class <class>) :rest 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 fucntions. 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 スロットアクセスのカスタマイズ).

If your metaclass needs to initialize auxiliary slots, you can define your own initialize method, in which you call next-method first to set up the core part of the <class> structure, then you sets up metcalss-specific part. One caveat is that, after next-method handes 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
Generic function: class-post-initialize class initargs

Next: , Previous: , Up: メタオブジェクトプロトコル   [Contents][Index]

7.5.2 スロットアクセスのカスタマイズ

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

これらふたつのジェネリックファンクションによって、クラスの持つべきスロット、 及び各スロットがどのようにアクセスされるかが決定されます。

クラスのinitializeメソッドは、クラスの direct-superscpldirect-slotsスロットを セットしてから、compute-slotsメソッドを呼びます。 このメソッドは既にセットされた3つのスロットの情報から、 該当クラスの持つべきスロットと、 各スロットのスロットオプションを決定します。 メソッドの返り値は以下の形式のフォームで、これがクラスのslotsスロットに セットされます。

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

compute-slotsの返り値によってslotsスロットが設定されたら、 次に各スロットについてcompute-get-n-setが呼ばれます。 このメソッドは、各スロットをどのようにアクセスするかを決定します。 引数はクラスとスロット定義(上の<slot-definition>)です。 返り値は以下のいずれかでなけばなりません。

整数n

このスロットはn番目のインスタンススロットになります。 インスタンスにスロットを割り当てる唯一の方法です。

compute-get-n-setのベースメソッドは、それまでに割り当てられた インスタンススロットの数をクラスのnum-instance-slotsスロットに格納しています。 他の特殊化されたメソッドでこのスロットの値を参照したり変更したりすることは 避けてください(オブジェクトシステムの中身を知悉していて、そうすべき十分な理由が ある場合は別ですが。) 通常の場合、単にnext-methodを呼び出せば、ベースメソッドが インスタンススロットを新たに割り当ててそのインデックスを返してくれます。

インスタンススロットアクセスのふるまいを変更する、下に示す例も参照してください。

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

Next: , Previous: , Up: メタオブジェクトプロトコル   [Contents][Index]

7.5.3 メソッドのインスタンシエーション

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

Previous: , Up: メタオブジェクトプロトコル   [Contents][Index]

7.5.4 メソッド適用のカスタマイズ

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

Previous: , Up: メタオブジェクトプロトコル   [Contents][Index]