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

7.2 クラス

この節では、Gauche におけるクラスについて詳しく説明します。


7.2.1 クラスの定義

クラスを定義するには、define-class マクロを使います。

Macro: define-class name supers (slot-spec …) option …

引数によって指定されたクラスオブジェクトを作成し、それを name に グローバルに束縛します。このマクロはトップレベルでのみ使うことができます。

Supers はそのクラスが継承する直接のスーパークラスのリストです。 多重継承も使えます。継承の詳細については継承 を参照して下さい。

Slot-spec は「スロット」の仕様で、他の言語ではよく 「フィールド」や「インスタンス変数」と呼ばれるものです (slot-spec を使って「クラス変数」を指定することもできます)。 slot-spec の最も単純なフォームはシンボルそのもので、その名前が スロットであるものです。あるいは、最初の要素がシンボルで残りの要素が キーワードと値が交互に来るリストを渡すこともできます。

このリストフォームは、スロットの名前を定義するだけでなく、そのスロットの 振る舞いも定義します。スロットの定義については以下で説明します。

最後に、option … は、クラスオブジェクトがどのように 作られるかを指定する、キーワードと値が交互に来るリストです。

このマクロでは1つのキーワード引数、:metaclass により、 メタクラス(他のクラスをインスタンス化するクラス)を指定できます。 他のオプションはクラスオブジェクトを作成するために、make メソッドに渡されます。メタクラスの使用方法については、 クラスのインスタンシエーションを参照。

スロットの指定はリストで、以下のようなフォームであるべきです。

(slot-name :option1 value1 :option2 value2 ...)

各キーワード(option1 など) は slot option を与えます。 デフォルトでは、以下のスロットオプションが認識されます。 メタクラスを定義することで、デフォルト以外のスロットオプションを 追加できます。

:allocation

このスロットのアロケーションタイプを指定します。これは、このスロットが どのように値を格納するかを指定します。以下のようなキーワード値が 標準クラスによって認識されます。プログラマは、自分用のメタクラスを定義し、 これら以外のアロケーションタイプを認識するように、このクラスを拡張すること ができます。

:instance

スロットは各インスタンス毎にアロケートされます。したがって、おのおのの インスタンスは別々の値をもてます。これは、いわゆる「インスタンス 変数」の振舞いを実現します。:allocation スロットオプションが 省略された場合、これがデフォルトとなります。

:class

スロットはクラスオブジェクト自身にアロケートされます。したがって、おのおのの インスタンスはこのスロットの同じ値を共有します。これは、いわゆる 「クラス変数」の振舞いを実現します。このスロットの値は、すべてのサブクラス でも共有されます。(ただし、サブクラスの定義がこのスロットをシャドウする 場合には、そのかぎりではありません。)

:each-subclass

class アロケーションと似ていますが、スロットはクラス毎にアロケート されます。すなわち、このスロットは、このクラスのすべてのインスタンスで 共有されますが、サブクラスのインスタンスには共有されません。

:virtual

このタイプのスロット用には格納領域はアロケートされません。このスロット にアクセスすると以下で説明する :slot-ref および :slot-set! オプションで与えられた手続きが呼ばれます。いいかえれば、手続きスロットを 作成できるということです。スロットアロケーションが virtual と指定されて いる場合、少なくとも :slot-ref オプションが同時に 指定されていなければなりません。さもなければ、define-class は エラーを発生させます。

:builtin

このアロケーションタイプは組み込みクラスの中だけに現れます。 Scheme 定義のクラスでこのタイプを指定することはできません。

:init-keyword

このスロットオプションに与えられたキーワード値は、インスタンスが生成 される際に make メソッドに初期値をわたすために使えます。

:init-value

生成時にキーワード引数で初期化されてないスロットの場合、これによって スロットの初期値を与えます。その値は define-class が評価される ときに、評価されます。

:init-form

init-value と似ていますが、与えられた値は thunk で包まれていて、 その値が必要とされた時に毎回評価されます。init-valueinit-form との両方が与えられた時には init-form が無視されます。 実際には、:init-form exprdefine-class マクロの :init-thunk (lambda () expr) に変換されます。

:initform

init-form と同義です。STk との互換性のためにあります。新しくコードを 書く場合には使うべきではありません。

:init-thunk

thunk を与えます。もし、当該スロットが生成時にキーワード引数によって 初期化されていなければ、その thunk を評価して当該スロットの初期値とします。 :init-formvalue を与えることと、:init-thunk(lambda () value) を与えることは同じことです。

:immutable

偽でない場合、このスロットは変更不可となります。 正確には、このスロットには一回だけ値をセットできます。通常それは初期化時に行われます。 スロットのセッターは、スロットが未束縛であった場合のみ成功し、そうでなければ エラーを投げます。 もしinitializeメソッド内でスロットの初期化が行われなかった場合は、 後で一回だけスロットに値をセットできます。これは遅延された初期化とみなせます。

:getter

シンボルをとり、getter メソッドを生成し、同じ名前のジェネリック関数に 束縛します。getter メソッドは当該クラスのインスタンスを引数とし、当該 スロットの値を返します。

:setter

シンボルをとり、setter メソッドを生成し、同じ名前のジェネリック関数に 束縛します。setter メソッドは当該クラスのインスタンスと値をひとつ引数 として、そのインスタンスの当該スロットの値をその値にセットします。

:accessor

シンボルをとり、2 つのメソッド(getter メソッドと setter メソッド)を 生成します。getter メソッドは与えられた名前のジェネリック関数に 束縛され、setter メソッドは、与えられた名前のジェネリック 関数のsetterとして追加されます(setter については 代入 を参照して下さい)。

:slot-ref

評価すると引数(インスタンス)を一つとる手続きとなる値を指定します。 このスロットオプションは当該スロットのアロケーションが virtual である場合、必ず指定されていなければなりません。プログラムが slot-refやgetter メソッドを使って 当該スロットの値を得ようとすると、 このオプションに指定された手続きが呼ばれ、その結果が 当該スロットの値として返されます。手続きは undef 値を返し (undefinedの戻り値)、スロットが値をもっていないことを示す ことができます。もしスロットのアロケーションが virtual で なければ、このスロットオプションは無視されます。

:slot-set!

評価すると二つの引数(インスタンスと値)をとる手続きとなる値を指定します。 プログラムがslot-set! あるいは setter メソッドを使って 当該スロットに値をセットしようとするときに、 指定した手続きがインスタンスとセットすべき値を引数として 呼ばれます。スロットアロケーションが virtualでなければ、このスロットオプションは無視されます。 このスロットオプションの無いvirtualスロットはリードオンリースロットとなります。

:slot-bound?

評価すると引数(インスタンス)を一つとる手続きとなる値を指定します。 このスロットオプションは当該スロットのアロケーションが virtual である場合しか意味を持ちません。 プログラムが当該スロットが値を持っているかどうかを決定しようとしたときに、 この手続きを呼ばれます。手続きは、スロットが値をもつなら、真の値を、 そうでなければ#f を返します。仮想スロットに対して、このスロット オプションが省略されると、システムは代りに slot-ref に与えられ た手続きを呼び、それが、#<undef> を返すかどうか見ます。


7.2.2 継承

継承にはふたつの役割があります。第一に、スロットを追加することで 既存のクラスを拡張できます。第二には、既存のクラス 関連するメソッドを特定化して、元々のメソッドよりもすこし 特定化した仕事をやらせるようにできます。

いくつかの用語を定義しておきましょう。クラス <T> がクラス <S>を継承しているとき、<T><S>サブクラスといい、<S><T>スーパークラスといいます。この関係は推移的です。すなわち、 <T> のサブクラスは、やはり <S> のサブクラスであり、 <S> のスーパークラスは、やはり <T> のスーパークラスです。 特に、<T><S> を直接継承している場合、すなわち、 <S><T> を定義する際のスーパークラスリストに 現われている場合には <S><T>直接スーパークラスといい、<T><S>直接サブクラスといいます。

クラスを定義したとき、そのクラスとそのスーパークラスは、サブクラスから スーパークラスという順序になり、クラスのリストがその順で生成されます。 このリストのことをクラス順位リスト、あるいは CPL といいます。 すべてのクラスはそれぞれ自身の CPL を持っています。 クラスの CPL は常に自分自身からはじまり、<top> で終ります。

手続き class-precedence-list を用いてクラスの CPL を問い合わせ ることができます。

gosh> (class-precedence-list <boolean>)
(#<class <boolean>> #<class <top>>)
gosh> (class-precedence-list <string>)
(#<class <string>> #<class <sequence>> #<class <collection>> #<class <top>>)

見るとわかるように、すべてのクラスは <top> という名前のクラスを 継承しています。組み込みクラスには、いくつかの抽象クラスを CPL 中で 自分自身と <top> の間に連ねているものもあります。上の例では <string> クラスは <sequence><collection> を 継承しています。これは、文字列がシーケンスとしても、コレクションとして も振舞うことができるということです。

gosh> (is-a? "abc" <string>)
#t
gosh> (is-a? "abc" <sequence>)
#t
gosh> (is-a? "abc" <collection>)
#t

Schemeで定義したクラスの継承についてはどうでしょう。 単一継承なら、CPL は直截的です。そのクラスのスーパークラス、 スーパークラスのスーパークラス、 スーパークラスのスーパークラスのスーパークラス、… と <top> に到達するまで、たどっていけます。例を見てください。

gosh> (define-class <a> () ())
<a>
gosh> (define-class <b> (<a>) ())
<b>
gosh> (class-precedence-list <b>)
(#<class <b>> #<class <a>> #<class <object>> #<class <top>>)

Scheme定義のクラスは常に <object> を継承します。 システムが自動的に挿入します。

多重継承が使われる場合には話はすこし複雑になります。複数のスーパークラスの 複数の CPL をひとつの CPL にマージしなければなりません。このことを 線形化といい、いくつかの線形化戦略が知られています。Gauche では デフォルトで C3 線形化と呼ばれているアルゴリズムを使います。 このアルゴリズムは局所的な順位、単調性、拡張順位グラフと整合性のとれた ものです。ここでは詳細に立ち入りませんが、一般的なルールとして、 CPL 中のスーパークラスの順序は、つねにそのクラスの直接スーパークラスの 順序、それぞれのスーパークラスの CPL の順序、および、各スーパークラス の直接スーパークラスの順序、などと整合性をもちます。正確な説明については 次の文献を参照してください: Kim Barrett, Bob Cassels, Paul Haahr, David A. Moon, Keith Playford, P. Tucker Withington, A Monotonic Superclass Linearization for Dylan, in Proceedings of OOPSLA 96, October 1996.

もしクラスが、整合性を満した CPL を構築できないようなやり方で 複数のスーパークラスを継承すると、エラーになります。以下は多重継承の 単純な例です。

(define-class <grid-layout> () ())

(define-class <horizontal-grid> (<grid-layout>) ())

(define-class <vertical-grid> (<grid-layout>) ())

(define-class <hv-grid> (<horizontal-grid> <vertical-grid>) ())

(map class-name (class-precedence-list <hv-grid>))
 ⇒ (<hv-grid> <horizontal-grid> <vertical-grid>
     <grid-layout> <object> <top>)

<hv-grid> の直接スーパークラス(<horizontal-grid><vertical-grid>)の順序が保存されていることに注意してください。

以下は、すこしひねくれた例です。

(define-class <pane> () ())

(define-class <scrolling-mixin> () ())

(define-class <scrollable-pane> (<pane> <scrolling-mixin>) ())

(define-class <editing-mixin> () ())

(define-class <editable-pane> (<pane> <editing-mixin>) ())

(define-class <editable-scrollable-pane>
   (<scrollable-pane> <editable-pane>) ())

(map class-name (class-precedence-list <editable-scrollable-pane>))
 ⇒ (<editable-scrollable-pane> <scrollable-pane>
     <editable-pane> <pane> <scrolling-mixin> <editing-mixin>
     <object> <top>)

いったんクラス順位が決まると、定義されたクラスのスロットが以下の手順で 計算されます。スロットの定義が CPL 中のスーパークラスからサブクラスへの 順で集められます。サブクラスにスーパークラスと同じ名前のスロット定義が あった場合には、サブクラスのそのスロット定義が採用され、スーパークラス の方の定義は捨てられます。あるクラス <S> がスロット ab、および c を定義しており、あるクラス <T> が スロット cd、および e を定義し、さらに、 あるクラス <U> がスロット b および e を定義していると しよう。<U> の CPL が (<U> <T> <S> <object> <top>) と なっている場合、<U> のスロットが下の図のように計算されます。 すなわち、<U> は 5 つのスロットをもち、 b および e の定義は <U> のものを、c および d の定義は <T> 由来のものを、そして、a の定義は <S> 由来のものとなります。

   CPL         | スロットの定義
               |  () はシャドウされたスロットを表す
 --------------+-------------------
   <top>       |
   <object>    |
   <S>         | a  (b) (c)
   <T>         |         c   d  (e)
   <U>         |     b           e
 --------------+--------------------
 <U>のスロット | a   b   c   d   e

class-slots 関数を使ってクラスオブジェクトのスロット定義の リストを得ることができます。

上述の振舞いはデフォルトの振舞いにすぎないことに注意してください。 CPL の計算方法あるいはスロット定義の継承方法は、メタクラスを 定義することでカスタマイズ可能です。たとえば、同じスロット名の スロットオプションはどれかが他のものをシャドウしますが、これを マージすることができるようにメタクラスを書くことができます。 あるいは、サブクラスがスーパークラスのスロットをシャドウするのを 禁止するようにメタクラスを書くことができます。


7.2.3 クラスオブジェクト

クラスとは何か。Gauche ではクラスはオブジェクトをインスタンス化して ある特定の機能を実装するようなオブジェクトにすぎません。 そうなので、スロットの値を見るだけで、クラス内部を覗けます。 このように内部を覗くのに便利な手続きがいくつか用意されています。 これらの手続きがリストを返す場合、それはクラスに所属するもので、 変更してはいけないということに注意してください。

Function: class-name class

class の名前を返します。

(class-name <string>) ⇒ <string>
Function: class-precedence-list class

class のクラス順位リストを返します。

(class-precedence-list <string>)
  ⇒ (#<class <string>>
      #<class <sequence>>
      #<class <collection>>
      #<class <top>>)
Function: class-direct-supers class

class の直接スーパークラスのリストを返します。 直接スーパークラスは class が直接継承しているクラスです。

(class-direct-supers <string>)
  ⇒ (#<class <sequence>>)
Function: class-direct-subclasses class

class の直接サブクラスのリストを返します。 直接サブクラスは class を直接継承しているクラスです。 <T><S> の直接サブクラスであれば、 <S><T> の直接スーパークラスです。

Function: class-slots class

classスロット定義 のリストを返します。スロット定義は リストで、その car 部はスロット名、cdr 部はスロットオプションを指定する キーワード値のリストです。スロット定義内部を覗いてスロットのもつ性格を 知ることができます。詳しくは スロット定義オブジェクト を 参照してください。

与えられたクラスのスロット名のリストを得るための標準的な方法は、 (map slot-definition-name (class-slots class)) です。

Function: class-slot-definition class slot-name

クラス class 中の slot-name で指定されたスロットの スロット定義を返します。class が指定した名前のスロットを 持たなければ #f が返ります。

Function: class-direct-slots class

当該クラスで直接定義されている(つまりスーパークラスから継承された ものではない)スロット定義のリストを返します。この情報は、クラスの 初期化の際にスロットの継承を処理するために利用されます。

Function: class-direct-methods class

classを特定化子中にもつメソッドのリストを返します。

Function: class-slot-accessor class slot-name

classslot-name で指定したスロットの スロットアクセサオブジェクトを返します。 スロットアクセサオブジェクトは内部オブジェクトで与えられたスロットへの アクセス方法、変更方法、初期化の方法という情報をカプセル化しています。

メタオブジェクトプロトコルを使って特別なスロットを定義するのでなければ、 通常スロットアクセサオブジェクトを扱う必要はありません。


7.2.4 スロット定義オブジェクト

class-slots が返すスロットの定義オブジェクト class-direct-slots および class-slot-definition はスロットに関する情報を保持しています。 現時点では Gauche はスロット定義を表現するのにリストを使っています。これは STklos や TinyCLOS と同じです。しかし、Gauche が将来にわたって、この構造を 保持するかどうかは保証のかぎりではありません。スロット定義オブジェクトの 情報を得るには以下のそれ専用のアクセサメソッドを使うべきです。

Function: slot-definition-name slot-def

スロット定義オブジェクト slot-def で与えられたスロットの名前を 返します。

Function: slot-definition-options slot-def

slot-def のスロットオプションのキーワード値リストを返します。

Function: slot-definition-allocation slot-def

slot-def:allocation オプションの値を返します。

Function: slot-definition-getter slot-def
Function: slot-definition-setter slot-def
Function: slot-definition-accessor slot-def

それぞれ、slot-def:getter:setter および :accessor スロットオプションの値を返します。

Function: slot-definition-option slot-def option :optional default

slot-def のスロットオプション option の値を返します。 そのようなオプションがない場合には、default が与えられていれば それを返し、さもなければ、エラーシグナルがあがります。


7.2.5 クラスの再定義

define-class を使うとき、指定したクラス名がすでにあるクラスに束縛 されている場合、これは元々のクラスの再定義と看倣されます。

クラスの再定義は以下の操作を意味します。

元々のクラスと新しいクラスは別のオブジェクトであることに注意してください。 元々のクラスオブジェクトは元々どのモジュールでどの変数に束縛されていたかを 覚えており、この束縛を新しいクラスに置き換えます。どこかで、元々のクラスへ の直接参照を持っていれば、その参照は元々クラスへの参照のままです。この点 については特に注意してください。class-redefinition メソッドを 定義することによりクラス再定義の振舞いをカスタマイズできます。 詳しくは メタオブジェクトプロトコルを参照してください。

元々のクラスのインスタンスが存在している場合、それらのインスタンスは 以下のようなメソッドでアクセスあるいは変更しようとしたときに自動的に 更新されます。class-ofis-a?slot-refslot-set!ref、getterメソッド、setterメソッド。

インスタンスの更新とは、インスタンスのクラスを(旧いクラスから新しいクラスへ) 変更するということです。デフォルトでは、元々のクラスと新しいクラスで 共通のスロットの値はそのまま引き継がれます。新しいクラスで追加された スロットは新しいクラスでのそのスロットの仕様にしたがって初期化されます。 元々のクラスから削除されたスロットの値は破棄されます。この振舞いは、 change-class メソッドを書くことでカスタマイズできます。詳しくは クラスの変更 を参照してください。

スレッド安全性について

クラス再定義処理はローカルな処理ではなく、多くの副作用を行います。 複数のスレッドが同時にクラス再定義プロトコルを走らせた場合の 安全性を保証するのは困難です。そこでGauche では、一度にひとつのスレッドしか クラス再定義プロトコルに入らないように、プロセスレベルのロックを使用します。

スレッドが、別のスレッドが再定義プロトコルにいる最中にクラスを 再定義しようとした場合、たとえ、別々のクラスを再定義しようとしている 場合でも、そのスレッドはブロックされます。このようにするのは、 再定義がそのすべてのサブクラス、そのクラスとそのサブクラスに 関わるすべてのメソッドおよびジェネリック関数に影響するからで、 ふたつのクラスが完全に独立であるかどうかを決定するのは、自明では ないからです。

スレッドが他のスレッドが再定義しようとしているクラスのインスタンスに アクセスしようとした場合にも、このスレッドは再定義が完了するまで、 ブロックされます。

インスタンス更新プロトコルは直列化されません。ふたつのスレッドが 再定義されたクラスのインスタンスにアクセスしようとすると、両方の スレッドが更新プロトコルを起動します。これは好ましくない競合状態を 生じる可能性があります。このような場合がおきないようにするのは アプリケーションの責任です。インスタンスへのアクセスはどのみち システムが直列化するわけではないので、これは自然なことです。 インスタンス内に mutex を持たせる場合は特に注意が必要です。 インスタンス中のmutexにアクセスするだけでインスタンス更新プロトコルを 起動することになる可能性があるからです。

互換性に関して

クラス再定義プロトコルは CLOS風の Scheme システムとは微妙に違います。 Gauche のものは STklos のものによく似ていますが、STklos 0.56 は再定義 サブクラスの束縛を置き換えず、初期化引数を覚えたりはしないので、 再定義されたサブクラスは、元々のサブクラスが持っていた情報のなにがしかを 失ってしまう可能性があるという点にちがいがあります。Guile のオブジェクト システムはクラス再定義プロトコルの最後で、元々のクラスのアイデンティティと 再定義されたクラスをアイデンティティを入れ替えてしまします。それゆえ、 元々のクラスへの参照は、再定義されたクラスへの参照となります。筆者が 知るかぎり、クラス再定義は、STklos 0.56 においても、Guile 1.6.4 においても スレッド安全ではありません。


7.2.6 クラスの定義例

いくつかの例をみましょう。グラフィカルツールキットを定義しているところ だということにしましょう。<window> はスクリーン上の矩形領域で、 幅と高さを持ちます。これを階層構造に構成することが可能です。すなわち、 ある window は別の window 中に置くことができ、親 window へのポインタを もっているものとします。window の位置は、親ウィンドウの位置からの 相対座標、x と y で指定します。スクリーン全体を覆う「ルート」window を作り、これがデフォルトの親 window にもなります。ここまでで、次のように なります。

;; The first version
(define-class <window> ()
  (;; Pointer to the parent window.
   (parent      :init-keyword :parent :init-form *root-window*)
   ;; Sizes of the window
   (width       :init-keyword :width  :init-value 1)
   (height      :init-keyword :height :init-value 1)
   ;; Position of the window relative to the parent.
   (x           :init-keyword :x :init-value 0)
   (y           :init-keyword :y :init-value 0)
   ))

(define *screen-width* 1280)
(define *screen-height* 1024)

(define *root-window*
  (make <window> :parent #f :width *screen-width* :height *screen-height*))

:init-value および :init-form の使い方に注意してください。 <window> クラスが定義されたとき、*root-window* はまだ束縛 されていませんので、ここでは :init-value は使えません。

gosh> *root-window*
#<<window> 0x80db1d0>
gosh> (define window-a (make <window> :width 100 :height 100))
window-a
gosh> (d window-a)
#<<window> 0x80db1b0> is an instance of class <window>
slots:
  parent    : #<<window> 0x80db1d0>
  width     : 100
  height    : 100
  x         : 0
  y         : 0
gosh> (define window-b
        (make <window> :parent window-a :width 50 :height 20 :x 10 :y 5))
window-b
gosh> (d window-b)
#<<window> 0x80db140> is an instance of class <window>
slots:
  parent    : #<<window> 0x80db1b0>
  width     : 50
  height    : 20
  x         : 10
  y         : 5

筆者と同じ感覚の持ち主なら、*root-window* のようなグローバル 変数をツールキットのユーザに見せたいとは思わないでしょう。これを カプセル化するひとつの方法は、ルート window へのポインタをクラス変数に 保持させることです。<window> の定義に以下のようなスロットオプションを 追加すると <window> クラスの root-window スロットは同じ 格納領域を参照するようになります。

(define-class <window> ()
  (...
   ...
   (root-window :allocation :class)
   ...))

<window> のインスタンスに対して、slot-ref および slot-set! を、また、<window> クラスそのものに対しては、 class-slot-refclass-slot-set! を使って root-window スロットの値を取得したり、セットしたりできます。

ツールキットのユーザは window の相対座標のかわりに絶対座標(ルートウィンドウ 中の座標)が欲しいと思うことがあるでしょう。以下のようにして、絶対座標を 返す仮想スロットを提供することができます。

(define-class <window> ()
  (...
   ...
   (root-x :allocation :virtual
           :slot-ref  (lambda (o)
                        (if (ref o 'parent)
                            (+ (ref (ref o 'parent) 'root-x)
                               (ref o 'x))
                            (ref o 'x)))
           :slot-set! (lambda (o v)
                        (set! (ref o 'x)
                              (if (ref o 'parent)
                                  (- v (ref (ref o 'parent) 'root-x))
                                  v)))
            )
    ...))

メソッドあるいは仮想スロット経由のこのようなインタフェースを供給することは いくぶんか趣味の問題です。仮想スロットは実装の変更を隠すことができるという 利点があります。つまり、root-x を実スロットに保持するように変更し、 x をあとで仮想スロットに変更するということを <window>を 使うコードをだめにすることなくおこなえます。オブジェクト指向の主流の言語 では、通常このような「実装変更の隠蔽」はインスタンス変数を隠し、メソッドを 公開するということでおこなわれています。Gauche やその他の CLOS 風システムでは スロットは常にユーザから見えており状況はすこし違うのです。



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