Gauche:MOP:クラスオブジェクトのchange-class
Shiro(2011/06/08 18:44:01 PDT): Gauche:Bugsより:
teppey(2011/05/29 17:56:03 PDT): メタクラスがどういうものかよく分かっていないのですが、以下のスクリプトがsegmentation faultになりました。
$ uname -srm Linux 2.6.32-5-amd64 x86_64 $ ./gosh -V Gauche scheme shell, version 0.9.1 [utf-8,pthreads], x86_64-unknown-linux-gnu $ cat /tmp/x.scm (define-class <c-meta> (<class>) ()) (define-class <c> () () :metaclass <c-meta>) (define c (make <c>)) (change-class <c> <c-meta>) (write c) $ ./gosh -ftest /tmp/x.scm zsh: segmentation fault ./gosh -ftest /tmp/x.scm
SEGVの直接の原因は、デフォルトのchange-classメソッド (実質はchange-object-class 手続き) が、builtinスロット(Schemeレベルで定義されたスロットではなく、 Cの構造体メンバがSchemeスロットとして見えているもの) をコピーしない、 というものだった。<class>オブジェクトの重要なスロットであるcplとか accessorsとかはbuiltinスロットなんで、これらがコピーされないと change-classした後の<class>オブジェクトは不完全で、 それを不完全なまま使おうとするとSEGVる。
で、そこを直して、クラスオブジェクトのchange-classについて若干 カスタマイズしてやれば上のコードは動くことは動くんだけど、 どう動くのが正しいか、ということについて考え出すとはまった。
Change-classの意味
インスタンスのchange-classは、
- インスタンスのidentityを変えずに
- その振る舞いや構造(スロットの数や名前)が変わる
というものだ。インスタンスの振る舞いや構造を決めるのはクラスなので、 クラスが変わればそれらが変わる、というのは意味的に納得できる。
で、構造を変えるためには、インスタンスの「変える前のクラス」と「変える先のクラス」 を比べて、例えば共通するスロットの値は持ち越すとか、 新しく追加されたスロットは初期化するとか、そういう操作が必要になる。
クラスオブジェクトはメタクラスのインスタンスなので、 その点ではインスタンスのchange-classと変わることはない。 つまり、クラスのidentityは変わらずに、その振る舞いや構造が 新たなメタクラスで指定されるものに変わる。
ここまでは何も問題がない。
ただ、クラスオブジェクトの場合、そのクラスから作られたインスタンス が既にあるかもしれない。そして、クラスオブジェクトが変化するということは、 既存のインスタンスの振る舞いや構造も変わる可能性がある。
だから、概念的には、クラスオブジェクトがchange-classされたら、 それはクラス再定義と同じようなことになると考えられる。 クラス再定義が起きると、既存のインスタンスはlazyに「前のクラス」から 「新たに定義されたクラス」へとchange-classされる。
ここが問題。クラス再定義の場合、「前のクラス」と「新たに定義されたクラス」は オブジェクトとしては別のものだ。だからchange-classが使える。 ところが、クラスオブジェクト自身がchange-classされた場合、 クラスオブジェクトのidentityはそのままで、情報が上書きされる。 「前のクラス」と「新たに定義されたクラス」の両方を 見比べることができない。
インスタンスがchange-classしようにも、そのインスタンスの現在のクラスと、 変えるべき先のクラスは、全く同一なのだ。
オプション
策はいくつか考えられる。それぞれについて帰結を考え中。
- クラスメタオブジェクトのchange-classを禁止する
- ただし、完全に禁止してしまうと、メタクラスの再定義ができなくなる (メタクラスの再定義はクラスのchange-classを引き起こす)。 メタクラスの再定義を特別扱いし、クラスのchange-classではなく クラスの再定義を引き起こすようにすれば回避できるかもしれない。
- インスタンスをアップデートしない
- クラスオブジェクトのchange-classによって起きた変化が、 インスタンスの構造に影響を与えないのであれば、インスタンスを アップデートする必要はない。インスタンスの振る舞い(メソッドの選択)を 決める、クラスのidentityは変わってないわけだから。
- ただ、もしクラスオブジェクトの変化がインスタンスの構造に影響を 与える場合 (クラスのchange-classが、そのクラスに強制的にslotを追加 したりするばあい)、インスタンスをアップデートしないと、 変化したスロットにアクセスにいってまずいことになる可能性がある。
- 回避策として、「インスタンスの構造に影響を与えるような クラスオブジェクトのchange-classのみ禁止する」というのはありかもしれない。
- 折衷案
- クラスオブジェクトのchange-classは、インスタンスの構造に影響を与えない 変更ならばクラスのidentityを保ったままの変更とし、 そうでなければ元のクラスオブジェクトは触らずにクラスの再定義が 引き起こされる (インスタンスは、通常のクラス再定義と同様、lazyにアップデートされる)
- これがleast surpriseかもしれない。ただ、全てのコーナーケースで うまく動くかどうか細かく検証する必要あり。
とりあえずコミット (4989fa2)
- インスタンスをアップデートしない、という方針を採用。
- クラスオブジェクトのchange-classで、インスタンスの構造を変えることを許さない、
ということにする。
- インスタンスの構造 (特に、インスタンススロットの数) は クラスのinitializeメソッドで決定されるんだが、現状ではchange-classでinitializeは 呼ばれないので、ただメタクラスを変更しただけでうっかりインスタンスの構造が 変わってしまう、ということはない。
- ただ、無理やりクラスの情報を書き換えればインスタンスの構造を変えることは可能。 それは実はchange-classに限らず、今までは実行時にいつでも可能だった。 そこで、クラスのクリティカルなスロットに関しては 「クラスメタオブジェクトがアロケートされてから、初期化が終了するまでしか変更できない」 というメカニズムを入れた。
- 将来、change-classの時に何らかの準初期化メソッド (CLOSにおける reinitialize-instanceみたいなの) を入れるとしたら、 その時に「クラスメタオブジェクトのreinitializeではインスタンスの構造を変えてはいけない」 って条件を入れる必要がある。