CLOS 風のオブジェクトシステムでは、オブジェクトシステムがそれ自身の上に
構築されます。すなわち、クラス構造のようなもの、クラスをどのように
生成するか、インスタンスをどのように生成し初期化するか、メソッドをどのように
ディスパッチし呼び出すか、これらはすべてオブジェクトシステムによって、
定義されます。たとえば、クラスはジェネリックな構造と標準的クラスの
振舞いを定義する <class>
クラスのインスタンスです。<class>
をサブクラス化すると、デフォルトのものとは違う振舞いをする、独自の
クラス集合をつくることができます。これは結局、独自のオブジェクトシステムを
つくることになります。
メタオブジェクトプロトコルは、どのようにオブジェクトシステムを 構築するかに関連する API 群の定義です。ブロック構築のクラス、オブジェクト システムを操作するあいだに呼ばれるジェネリック関数の名前と順序などです。 これらのクラスをサブクラス化し、これらのメソッドを特定化することは、 オブジェクトシステムの振舞いをカスタマイズすることを意味します。
• クラスのインスタンシエーション: | ||
• スロットアクセスのカスタマイズ: | ||
• メソッドのインスタンシエーション: | ||
• メソッド適用のカスタマイズ: | ||
• クラスの再定義のカスタマイズ: |
すべてのクラスはある特殊なクラスのグループのインスタンスになっています。
他のクラスのクラスになれるようなクラスのことを メタクラス と呼びます。
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 は以下のルールで 決定されます。
:metaclass
オプションが define-class
マクロに
与えられていれば、その値を使います。その値は、<class>
クラスか
あるいはその子孫でなければなりません。
<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>
: name ¶クラスの名前、define-class
マクロに与えられたシンボルです。
class-name
はこの値を返します。
<class>
: cpl ¶クラス順位リストです。class-precedence-list
はこの値を返します。
<class>
: direct-supers ¶直接スーパークラスのリストです。
class-direct-supers
はこの値を返します。
<class>
: accessors ¶スロットアクセサの連想リストです。これは各スロットがどのようにアクセスされる べきかをカプセル化しています。
<class>
: slots ¶スロット定義のリストです。class-slots
はこの値を返します。
スロット定義についての詳細は、スロット定義オブジェクト を参照してください。
<class>
: direct-slots ¶このクラスの定義で直接指定された(つまり継承したものではない)スロット定義の
リストです。class-direct-slots
はこの値を返します。
<class>
: num-instance-slots ¶インスタンスにアロケートされるスロットの数です。
<class>
: direct-subclasses ¶このクラスを直接継承しているクラスのリストです。
class-direct-subclasses
はこの値を返します。
<class>
: direct-methods ¶このクラスを特定化子リスト中にもつメソッドのリストです。
class-direct-methods
はこの値を返します。
<class>
: initargs ¶このクラスが生成されるときの初期化引数リストです。この情報は 再定義されたクラスを初期化するのに使います(クラスの再定義 参照)。
<class>
: defined-modules ¶このクラスがグローバル束縛をもつモジュールのリストです。
<class>
: redefined ¶このクラスが再定義された場合、このスロットは新しいクラスへの参照を含みます。
そうでない場合にはこのスロットは #f
をもっています。
<class>
: category ¶このスロットの値は、このクラスがどのように生成されたかを示しています。
Scheme 定義のクラスは、scheme
というシンボルを持っています。それ以外の
値は内部的に使用するだけです。
<class>
用の initialize メソッド ¶define-class
マクロは、(make <class> …)
という呼び出しへと
展開されます。make
はクラスメタオブジェクトをアロケートし、
そのinitialize
メソッドを呼びます。
このメソッド内で、クラスの継承順位(class precedence list)や
持つべきスロットが計算され、クラス内部のスロットに適切な値が与えられます。
そしてこのメソッドの末尾で、クラスの重要なスロットが凍結され、
以降の変更が禁止されます。
継承とスロットの計算はジェネリックファンクションにより行われます。
メタクラスを定義し、それに適切なメソッドを定義することで、
振る舞いをカスタマイズできます。クラスの継承は、下に示す
compute-cpl
メソッドにより計算されます。
スロットの計算はもうちょっと複雑なので、次のサブセクションで説明します
(スロットアクセスのカスタマイズ)。
クラスが独自のスロットを持っていて初期化が必要な場合は、
メタクラスにinitialize
メソッドを定義し、
その中でまずnext-method
を呼んで<class>
構造の基本部分部分を
初期化してから、独自部分を初期化します。
next-method
により<class>
の基本部分が初期化されたら、
それはもう変更できないことに注意してください。
基本部分のスロットに手を加えたい場合は、
クラスの基本部分が凍結される直前に呼ばれる
class-post-initialize
メソッドをオーバライドすることにより可能です。
<class>
に定義されているinitialize
メソッドから呼ばれ、
クラスの継承優先順リスト(class precedence list, CPL)を計算する
ジェネリックファンクションです。
これが呼ばれる時点で、クラスメタオブジェクト
classはname
およびdirect-supers
スロットだけが
設定されています。direct-supers
スロットの値はclassが直接継承する
クラスのリストです。ここに含まれるクラスは全て初期化済みです。
返り値はクラスのリストで、classから始まり、<top>
で終わっていなければ
なりません。このリストの順に、マッチするメソッドが探されます。
<class>
に対して定義されているメソッドはC3線形化アルゴリズムを使用します。
これは、直接間接を問わず継承している全てのクラスをトポロジカルソートするものです。
CPLの計算をカスタマイズしたい場合にこのメソッドをオーバライドしてください。 計算のアルゴリズムを変えることは、他のオブジェクトシステムをエミュレートするというのでも なければあまり必要がないかもしれませんが、例えば自分のメタクラスでは 必ずあるクラスを継承することを保証する、といった使い方もできます。
このジェネリックファンクションは、classのコア部分の初期化が終わった後、 クラスが「凍結」される、つまくクラスの主要なスロットが変更不可になる直前に呼ばれます。 このメソッドをオーバライドすることで、オブジェクトシステムを「騙す」ことができます。
自分が何をしているかわかっている場合のみ、このメソッドをオーバライドしてください。 オブジェクトシステムは、クラスのコア部分が標準的な方法で設定されていることを前提に 動作します。それを変えることは容易にシステムを壊します。
これらふたつのジェネリックファンクションによって、クラスの持つべきスロット、 及び各スロットがどのようにアクセスされるかが決定されます。
クラスのinitialize
メソッドは、クラスの
direct-supers
、cpl
、direct-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番目のインスタンススロットになります。 インスタンスにスロットを割り当てる唯一の方法です。
compute-get-n-setのベースメソッドは、それまでに割り当てられた
インスタンススロットの数をクラスのnum-instance-slots
スロットに格納しています。
他の特殊化されたメソッドでこのスロットの値を参照したり変更したりすることは
避けてください(オブジェクトシステムの中身を知悉していて、そうすべき十分な理由が
ある場合は別ですが。)
通常の場合、単にnext-method
を呼び出せば、ベースメソッドが
インスタンススロットを新たに割り当ててそのインデックスを返してくれます。
インスタンススロットアクセスのふるまいを変更する、下に示す例も参照してください。
(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.
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.)
If this element is #f
or omitted, the slot becomes read-only;
any attempt to write to the slot will raise an error.
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>
.
:init-value
or :init-form
.
<slot-accessor>
objectAccess 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.
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.
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.
The standard processes the slot definition with the following
slot allocations: :instance
, :class
,
each-subclass
and :virtual
.
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
クラスが再定義されると(クラスの再定義参照)、
まず新たなクラスメタオブジェクトが
(make metaclass initargs …)
によって生成され、
それからこのジェネリックファンクションが旧クラスメタオブジェクトと
新クラスメタオブジェクトを引数にして呼ばれます。
このジェネリックファンクションは、旧クラスの必要な情報を
新クラスに移行する役割を担います。
デフォルトメソッド、(class-redefinition <class> <class>)
は、
旧クラスを参照している全てのメソッドを更新し、
スーパークラスとサブクラスに変更を伝搬します。
このジェネリックファンクションをカスタマイズする際には、
next-method
でデフォルトメソッドを呼び出して、
基本的な更新操作が行われるようにしてください。デフォルトメソッドが呼ばれなかった場合、
意図しない影響が現れるかもしれません。
クラス再定義はたくさんの構造を変更します。途中でエラーを投げて中断してしまった場合、 内部状態の一貫性が失われるかもしれません。
内部的に、システムはクラス再定義のためのグローバルなmutexを持っていて、
再定義はたかだか一つのスレッドで処理されるようになっていあす。
従ってclass-redefinition
の途中で他のスレッドが割り込んでクラス構造が
変更する心配はしなくて大丈夫です。
(他の操作に関してはマルチスレッドが走っているので、
クラス再定義に関する構造以外の構造を触る時は排他制御が必要です。)