Gauche:GenericFunctionとModule

Gauche:GenericFunctionとModule

Stklosの流れを汲むオブジェクトシステム(Guile, Gaucheも含む)を、 モジュールシステムとともに使う際の問題点について検討する。

implicit generic function creation

CLOS系のオブジェクトシステムでは、メソッドはクラスにではなく 同名のgeneric functionに所属する。

Tiny CLOS等、単純なシステムではメソッドを作る前にあらかじめ generic functionを作成しておかねばならない。しかし、新しい名前の メソッドを定義するたびにgeneric functionを作るのは手間だ。 そこで、CLOSやSTklosでは、メソッド作成時に同名のgeneric function が定義されていないかどうかをチェックし、無ければ自動的に作成する ことになっている。STklosの流れを汲むGaucheやGuile(goops)も同じだ。

ここで「同名のgeneric function」と書いたが、Schemeの作法では generic functionに名前をつけるというより、generic functionそのものは 無名のオブジェクトで、それが特定の変数に束縛されていると考えた 方がすっきりする。STklosでもそうなっている。したがって、

  (define-method foo (...) ...)

というフォームがあった場合、マクロdefine-methodが展開される コードでは、グローバル変数 'foo' の束縛を調べ、それがgeneric function に束縛されていれば新たに作成するメソッドをそのgeneric functionに 追加し、'foo'が束縛されていなければgeneric functionを作成して 'foo' に束縛し、メソッドをそれに追加するという操作を行う。

ところで、 もともと全てをfirst classの無名オブジェクトにしたがるSchemeにおいて、 「名前」で関連づけるシステムは少々座りが悪い。 さらに、このシステム、module systemとの相性が極めて悪いのだ。

モジュールAから見える範囲で、変数fooは束縛されていないとする。 ここで、モジュールA内でメソッドfooを定義すれば、 新たにgeneric functionが作成され、それがモジュールA内でfooに束縛される。

次に、モジュールAとは無関係のモジュールBがあり、モジュールBから 見える範囲でやはりfooは束縛されていないものとする。 ここでメソッドfooを定義すると、やはり新たにgeneric functionが 作成され、それがモジュールB内でfooに束縛される。 新しく作られたgeneric functionは当然、上で作られたgeneric function とは関係の無いオブジェクトである。

さて、モジュールCからモジュールAとモジュールBをimportして、 メソッドfooを呼び出したらどうなるか。 モジュールA内で定義されたメソッドfooのspecializerと、 モジュールB内で定義されたメソッドfooのspecializerが異なれば、 直観的にはCからのメソッド呼び出しは引数によってA内で定義されたメソッドか B内で定義されたメソッドにディスパッチされると考えられるだろう。 しかし実際は、C内で評価したfooはモジュールAで作られたgeneric function かモジュールBで作られたgeneric functionかのどちらかを返し、 それはもう一方のメソッドのことは知らないのだ。

Gaucheで書けば次のようになる。

   file A.scm
   --------------
   (define-module A
     (export foo)
     (define-method foo ((obj <number>))
       (format #t "Module A ~s
" obj)))
   file B.scm
   --------------
   (define-module B
     (export foo)
     (define-method foo ((obj <string>))
       (format #t "Module B ~s
" obj)))
   file C.scm
   --------------
   (define-module C
     (import A)
     (import B)
     (foo 2)           ;; Aで定義したfooが呼ばれて欲しい→呼ばれる
     (foo "aa")        ;; Bで定義したfooが呼ばれて欲しい→エラー
     )

Guileは少々文法が違うが、同様の問題が見られる。

   file A.scm
   --------------
   (define-module (A)
     :use-module (oop goops)
     :export (foo))
   (define-method (foo (obj <number>))
     (format #t "Module A ~s
" obj))
   file B.scm
   --------------
   (define-module (B)
     :use-module (oop goops)
     :export (foo))
   (define-method (foo (obj <string>))
     (format #t "Module B ~s
" obj))

何が困るのか

「そもそも、モジュールAとモジュールBは無関係でたまたま 名前が衝突しただけだ。普通の束縛で名前が衝突したら片方の束縛が もう一方の束縛をシャドウするのだから、上で示した動作はむしろ 理にかなっている」と考えることもできる。

しかし、CLOS系オブジェクトシステムでは、クラス定義時に スロットのアクセサメソッドの名前を与えることが出来る。

  (define-class <foo> ()
    ((name  :accessor get-name)))
  (define-class <bar> ()
    ((name  :accessor get-name)))

こうすれば、get-nameというgeneric functionによってオブジェクト <foo>のnameスロットにも<bar>のnameスロットにもアクセスできる わけだ。このクラス定義のフォームはマクロ展開され、 get-nameメソッドを定義するフォームが生成されて実行されるから、 これらのクラス定義の時点でget-nameが束縛されていなければ、 新たにgeneric functionが作成される。

ここで、<foo>と<bar>が別モジュールにあったら、 まさに上で述べたような事態が発生する。 この場合、メソッドもimplicitに作られているため、 非常にわかりにくいバグになる可能性が高い。 そして、このバグは<foo>や<bar>の作成者があらかじめ防ぐことができない。

nameというスロットはよくあるし、そのアクセサ名が重なるのも ありがちなことだ。generic functionのメカニズムがあるから 名前の衝突を気にせずにアクセサ名を付けることができるとも言える。 それなのに、同様に名前の衝突を回避する目的のmodule systemによって こういう羽目になるのは困ったことだ。

(Common Lispではなぜこれが問題にならないかというと、 Common Lispのパッケージシステムは「束縛」ではなく「名前そのもの」 を管理しているからだと思う。パッケージA内で定義したfooとパッケージB内で 定義したfooは、A:fooとB:fooという別々のシンボルに格納されることになる。 パッケージCからAとBを何も考えずにインポートしようとすると 「名前がぶつかる」と言われてエラーになる。パッケージC作成者が 意識的に片方をシャドウするか、片方からfooというシンボルのインポートを あきらめるしかない。それでも依然としてCからは、A:fooあるいはB:foo の名でもともとのgeneric functionにアクセス可能だ。)

「モジュールを全て先に評価して…」というのは、 「全ての束縛が実行前に解決されている」と考えて良いでしょうか (「exportされなかったもの以外ひとつのモジュールに属して…」 というのはできないです。同名のシンボルを定義してimportしている シンボルをシャドウする場合があるので)。 それなら、モジュール毎にgeneric functionのインスタンスを作成し、 シャドウする束縛を生成しておくことで可能です。すなわち上記の例では、

というふうになります。fooの実体がひとつではまずい理由は、 モジュールB内で (foo 3) のように呼んだ場合、無関係であるはずの モジュールAで定義されたメソッドが呼ばれてしまうからです。

しかし、Gaucheのようなダイナミックな環境の場合、モジュールCが 読み込まれた後でモジュールAに定義を追加したりできてしまうんですね。 また、generic functionの実体がいくつもあると、gfをテーブルに 格納しておいてディスパッチするようなコードがモジュールをまたぐと うまく動かなくなります。

generic functionが呼ばれる度に、そのモジュールから可視な メソッドだけをディスパッチの対象に入れるようにすれば完璧ですが、 実行ペナルティが大きすぎます。

妥協策として、上記の「見えないはずのメソッドが呼ばれてしまう」現象に 目をつむるならば、implicitに作られたgeneric functionは 常に全てのモジュールが継承しているモジュール中に束縛が作られる、 という方法はあります。今のGaucheにある、gauche.gfというモジュールは そのつもりで作ったものです。例えば、 (define-method foo ...)した時点でfooが見えなければ、gauche.gf中に fooという名でgeneric functionを束縛し、それにメソッドを追加します。 gauche.gfは通常のモジュールは全て継承しているので、自動的にfooは 全てのモジュールから見えるようになります(そのモジュールでシャドウして いなければ、ですが)。

ただ、この妥協策はモジュールのモジュラリティを大きく損なうので、 導入に躊躇しているんですよね…

importのnon-transitivity

現行のモデル(0.6.6)ではモジュール絡みで別の問題が発生することに気づいた。

理由:モジュールbar中で define-method foo-funcした時、作成されたメソッドは モジュールfoo中のgeneric functionへと追加されるが、 foo-funcへの束縛はモジュールbar中には追加されない。 (define-methodは既にgeneric functionが存在すれば単なるadd-method!へと 展開される)。 モジュールのimportは遷移的ではないので、モジュールappはモジュールbar中の 束縛は調べるが、さらにbarのimportを辿ってfooを見に行くことは無い (appがfooを直接importしていない限り)。そして、 いくらexportされていてもbar中に束縛そのものが発見できないので appからのfoo-funcの参照はエラーになる。

現時点での回避策:いくつかあるが、いずれもモジュラリティを損なう方法 なのであまり好ましくはない。

ad hocな解決としては、define-method中で常にcurrent-moduleにも 束縛を挿入するようにすれば良いんだが、あまり綺麗ではない。

むしろ、前章に述べたような名前空間の問題も一緒に解決する解がありそうだし、 それが綺麗な解になるような気がする。

More ...