トレイトにおける、メソッドコンフリクト時の対処のしかた


多重継承機構を利用する際の問題としてよく取りざたされる「メソッド名のコンフリクト(衝突)」ですが、Squeak Smalltalk のトレイト機構(Traits)では、同種のミックスイン機構の場合と異なり、その対処はユーザーに委ねられます。


たとえば、いずれもメソッド #m1 と #m2 を持つ T1 と T2 というトレイト、両者を同時に use するクラス C があったとき、

Trait named: #T1
   uses: {}

T1 >> m1   ^'T1>>#m2'
T1 >> m2   ^'T1>>#m2'

Trait named: #T2
   uses: {}

T2 >> m1   ^'T2>>#m1'
T2 >> m2   ^'T2>>#m2'

Object subclass: #C
   uses: T1 + T2


トレイト機構は、C >> #m1(および #m2) に対して、

C >> m1
   self traitConflict

というような内容のメソッドを自動的に生成して関連づけます。

これは、ユーザーに対して何らかの対処が必要であることを知らせると同時に、もし、対処を忘れて放置されたままコールされてしまうような場合でも、注意を喚起すべく「Error: A class or trait does not properly resolve a conflict between multiple traits it uses.」というエラーを発生する式にもなっています。

C new m1   "=> Error "

他方で、たとえば Ruby のモジュール機構では、モジュールを include した順に上書き(メソッド検索経路の手前?に挿入)するルールなので、こうしたエラーを発するメソッドを動的に生成したり、あるいは直接エラーを出すことで、コンフリクトが明示的にされることはありません。

module M1
  def m1; "M1#m1" end
  def m2; "M1#m2" end
end

module M2
  def m1; "M2#m1" end
  def m2; "M2#m2" end
end

class C1
  include M1, M2
end

class C2
  include M2, M1
end
C1.new.m1   #=> "M1#m1"
C1.new.m2   #=> "M1#m2"

C2.new.m1   #=> "M2#m1"
C2.new.m2   #=> "M2#m2"

C1.ancestors  #=> [C1, M1, M2, Object, Kernel]
C2.ancestors  #=> [C2, M2, M1, Object, Kernel]


CLOS でも同様です。追記:CLOS でコンフリクトが起こる場合の対処については、shiro さんにコメントをいただきました。ありがとうございます。

(defclass <b1> () ())
(defmethod f1 ((_ <b1>)) "#<F1 (#<B1>)>")
(defmethod f2 ((_ <b1>)) "#<F2 (#<B1>)>")

(defclass <b2> () ())
(defmethod f1 ((_ <b2>)) "#<F1 (#<B2>)>")
(defmethod f2 ((_ <b2>)) "#<F2 (#<B2>)>")

(defclass <c1> (<b1> <b2>) ())
(defclass <c2> (<b2> <b1>) ())
(f1 (make-instance '<c1>))   ;;=> "#<F1 (#<B1>)>"
(f2 (make-instance '<c1>))   ;;=> "#<F2 (#<B1>)>"

(f1 (make-instance '<c2>))   ;;=> "#<F1 (#<B2>)>"
(f2 (make-instance '<c2>))   ;;=> "#<F2 (#<B2>)>"


ちなみに、Squeak Smalltalk のトレイト機構にも、いちおう use するトレイトの合成物(トレイトコンポジション。a TraitComposition)、

(T1 + T2) class   "=> TraitComposition "

の生成の際に順序をつけることはできるようなのですが、

Object subclass: #C
   uses: T1 + T2
Object subclass: #C
   uses: T2 + T1


いずれにせよ、けっきょく #m1、#m2 についてはコンフリクトと判断されることに変わりはないので、少なくともこのケースにおいて両者の区別に意味はなさそうです。



ここでふと、他の変わった例はないかなー、SELF はたしか検索経路にプライオリティを付けてメソッド(もしくは属性)の検索順序を制御するという、結構凝ったことをしていたよな…ということを思い出したので調べてみたところ、現在はこの試みは誤りだったと認め、コンフリクトがあればエラーを出し、曖昧さの解消はコールしたいメソッドを直接ユーザーに指定させるという素朴なものに落ち着いているようです。がっかりだよっ!w

mixins _AddSlots: (|

   mx1 = (|
      m1 = (^'mx1 m1').
      m2 = (^'mx1 m2').
   |).

   mx2 = (|
      m1 = (^'mx2 m1').
      m2 = (^'mx2 m2').
   |).

|)
globals _AddSlots: (|
   obj = (|
      mx1* = mixins mx1.
      mx2* = mixins mx2.
   |)
|)
obj m1       "=> Error: More than one 'm1' slot was found in a slots object. "

obj mx1 m1   "=> 'mx1 m1' "
obj mx1 m2   "=> 'mx1 m2' "

obj mx2 m1   "=> 'mx2 m1' "
obj mx2 m2   "=> 'mx2 m2' "


くしくも C++ と同じ。

#include <iostream>
using namespace std;

class B1 {
public:
    void f1() { cout<<"B1::f1"<<endl; }
    void f2() { cout<<"B1::f2"<<endl; }
};

class B2 {
public:
    void f1() { cout<<"B2::f1"<<endl; }
    void f2() { cout<<"B2::f2"<<endl; }
};

class C : public B1, public B2 { };

int main() {
    C c;

    //  c.f1();   //=> error: request for member 'f' is ambiguous

    c.B1::f1();   //=> B1::f1
    c.B1::f2();   //=> B1::f2

    c.B2::f1();   //=> B2::f1
    c.B2::f2();   //=> B2::f2

    return 0;
}

さて。コンフリクトが生じてしまった場合、Ruby や CLOS の多重継承機構とは違って、トレイト機構は、実質的には解決のためのなにもしてくれない(かといって、SELF や C++ のようにそれぞれを直接コールできるといったわけでもない…)ので、ユーザーは、採用するひとつを残して他のトレイトから同名のメソッドを読み込まないように排除するか、改めて同名のメソッドを再定義するかのいずれかのアクションを起こさねばなりません。

実際に、use するトレイトから特定のメソッドを読み込まないようにするには、そのトレイトに「- {#m1}」というように、メソッド名(シンボル)を { } でくくって引数として添えたメッセージを送って #- をコールしてやります。

Object subclass: #C1
   uses: T1 - {#m2} + T2 - {#m1}   " T1 から #m2 を、T2 から #m1 を排除 "

Object subclass: #C2
   uses: T1 - {#m1} + T2 - {#m2}   " T1 から #m1 を、T2 から #m2 を排除 "
C1 new m1   "=> 'T1>>#m1' "
C1 new m2   "=> 'T2>>#m2' "

C2 new m1   "=> 'T2>>#m1' "
C2 new m2   "=> 'T1>>#m2' "


Ruby や CLOS と違って、メソッド単位で判断ができるのが(不便と表裏の)利点です。Ruby でも undef を使えば似たようなことはできるのかな…と思ったのですが、undef は いったん使うと以降は 同名のメソッドのコールがことごとく NoMethodError になってしまうので、alias のほうが近いことを実現可能なのかもしれません。

class C1_bis
  include M1
  alias :_m1 :m1
  include M2
  alias :m1 :_m1
end

class C2_bis
  include M2
  alias :_m1 :m1
  include M1
  alias :m1 :_m1
end
C1_bis.new.m1   #=> "M1#m1"
C1_bis.new.m2   #=> "M2#m2"

C2_bis.new.m1   #=> "M2#m1"
C2_bis.new.m2   #=> "M1#m2"


ところで別名(エイリアス)機能については、トレイト機構にも(排除機能とは別に)用意されていて、注目するトレイトに対して「@ {#aliasOfM1->#m1}」というような、別名 -> オリジナル名というアソシエーション(関連づけオブジェクト)を列挙した配列を引数として添えたメッセージを送信し、#@ をコールしてやることで利用できます。

ただ、Ruby の alias と同様に、トレイトの別名機能は、別名の*追加*で、名前の*付け替え*ではありません。したがって、相変わらずコンフリクトは解消していないので、あらためて #- をコールすることで、コンフリクトの原因メソッドをいちいち排除する必要があります。

Object subclass: #D1
   uses: T1 @ {#m2ofT1->#m2} - {#m2} + T2 @ {#m1ofT2->#m1} - {#m1}
D1 new m1       "=> 'T1>>#m1' "
D1 new m1ofT2   "=> 'T2>>#m1' "

D1 new m2       "=> 'T2>>#m2' "
D1 new m2ofT1   "=> 'T1>>#m2' "

ところで、現行の Squeak システムで、トレイト機構が自動生成するコンフリクトメソッドオブジェクトには、二つめ以上の生成で、既存のメソッドオブジェクトに対してセレクタ(メソッド名)が正しく関連づけされない…という問題があるようです。このため、ブラウザで当該メソッドを選択してソースをみようとすると、でっかいポップアップが突如現われて「ソースファイル(.source や .changes …)が壊れている」というような旨の警告を再三発してきます。これは遊んでいてさすがにうざいので、ちょっと直してみたのがこちら。根っこは、メソッドオブジェクトで最近追加(強化?)されたプラグマ周辺のバグのようです。