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

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

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

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


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. さもなければ、クラス順位リスト中のクラスのメタクラスが試されます。
    • もし、すべてのメタクラスが <class> であるなら、生成される クラスのメタクラスも <class> になります。
    • もし、すべてのメタクラスが <class> かあるいは別のメタクラス A のどちらかであれば、生成されるクラスのメタクラスは、A に なります。
    • もしメタクラスの集合が <class> 以外の 2つ以上のメタクラス (A, B, C …)を含む場合、生成されるクラスの メタクラスはこれらのメタクラス A, B, C … すべてを 継承したメタクラスになります。

クラスの名前、スーパークラス、スロットの定義は初期化引数として ジェネリック関数 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>) initargs

define-classマクロは、(make <class> …)という呼び出しへと 展開されます。makeはクラスメタオブジェクトをアロケートし、 そのinitializeメソッドを呼びます。 このメソッド内で、クラスの継承順位(class precedence list)や 持つべきスロットが計算され、クラス内部のスロットに適切な値が与えられます。 そしてこのメソッドの末尾で、クラスの重要なスロットが凍結され、 以降の変更が禁止されます。

継承とスロットの計算はジェネリックファンクションにより行われます。 メタクラスを定義し、それに適切なメソッドを定義することで、 振る舞いをカスタマイズできます。クラスの継承は、下に示す compute-cplメソッドにより計算されます。 スロットの計算はもうちょっと複雑なので、次のサブセクションで説明します (スロットアクセスのカスタマイズ)。

クラスが独自のスロットを持っていて初期化が必要な場合は、 メタクラスにinitializeメソッドを定義し、 その中でまずnext-methodを呼んで<class>構造の基本部分部分を 初期化してから、独自部分を初期化します。 next-methodにより<class>の基本部分が初期化されたら、 それはもう変更できないことに注意してください。 基本部分のスロットに手を加えたい場合は、 クラスの基本部分が凍結される直前に呼ばれる class-post-initializeメソッドをオーバライドすることにより可能です。

Generic function: compute-cpl class

<class>に定義されているinitializeメソッドから呼ばれ、 クラスの継承優先順リスト(class precedence list, CPL)を計算する ジェネリックファンクションです。

これが呼ばれる時点で、クラスメタオブジェクト classnameおよびdirect-supersスロットだけが 設定されています。direct-supersスロットの値はclassが直接継承する クラスのリストです。ここに含まれるクラスは全て初期化済みです。

返り値はクラスのリストで、classから始まり、<top>で終わっていなければ なりません。このリストの順に、マッチするメソッドが探されます。 <class>に対して定義されているメソッドはC3線形化アルゴリズムを使用します。 これは、直接間接を問わず継承している全てのクラスをトポロジカルソートするものです。

CPLの計算をカスタマイズしたい場合にこのメソッドをオーバライドしてください。 計算のアルゴリズムを変えることは、他のオブジェクトシステムをエミュレートするというのでも なければあまり必要がないかもしれませんが、例えば自分のメタクラスでは 必ずあるクラスを継承することを保証する、といった使い方もできます。

Generic function: class-post-initialize class initargs

このジェネリックファンクションは、classのコア部分の初期化が終わった後、 クラスが「凍結」される、つまくクラスの主要なスロットが変更不可になる直前に呼ばれます。 このメソッドをオーバライドすることで、オブジェクトシステムを「騙す」ことができます。

自分が何をしているかわかっている場合のみ、このメソッドをオーバライドしてください。 オブジェクトシステムは、クラスのコア部分が標準的な方法で設定されていることを前提に 動作します。それを変えることは容易にシステムを壊します。


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

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

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

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

7.5.5 クラスの再定義のカスタマイズ

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

クラスが再定義されると(クラスの再定義参照)、 まず新たなクラスメタオブジェクトが (make metaclass initargs …)によって生成され、 それからこのジェネリックファンクションが旧クラスメタオブジェクトと 新クラスメタオブジェクトを引数にして呼ばれます。 このジェネリックファンクションは、旧クラスの必要な情報を 新クラスに移行する役割を担います。

デフォルトメソッド、(class-redefinition <class> <class>)は、 旧クラスを参照している全てのメソッドを更新し、 スーパークラスとサブクラスに変更を伝搬します。 このジェネリックファンクションをカスタマイズする際には、 next-methodでデフォルトメソッドを呼び出して、 基本的な更新操作が行われるようにしてください。デフォルトメソッドが呼ばれなかった場合、 意図しない影響が現れるかもしれません。

クラス再定義はたくさんの構造を変更します。途中でエラーを投げて中断してしまった場合、 内部状態の一貫性が失われるかもしれません。

内部的に、システムはクラス再定義のためのグローバルなmutexを持っていて、 再定義はたかだか一つのスレッドで処理されるようになっていあす。 従ってclass-redefinitionの途中で他のスレッドが割り込んでクラス構造が 変更する心配はしなくて大丈夫です。 (他の操作に関してはマルチスレッドが走っているので、 クラス再定義に関する構造以外の構造を触る時は排他制御が必要です。)



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