Scalaのトレイトは実はトレイトじゃなくただのミクスイン

タイトルは釣りです。


まずおおざっぱに用語の整理をさせていただくと、ここで「トレイト」は、シェルリ(Nathanael Schärli)らが2002年頃に発表したTraitsやそれ用のエンティティ(trait)を指し、「ミクスイン(Mixin, mixin)」は従来からある実装の多重継承方法のひとつ、具体的には継承機構を使ってメソッドを定義したクラス様エンティティ(クラスでも構わない)を継承パスに差し込むことで対象となるクラスにメソッドを追加する機構(特別な機構を要しないときは単なるクラスの運用方法)、そのときに用いるクラスあるいはクラス様エンティティ(例えばRubyならモジュールとか)を指すことにします。

トレイトやその機構について説明すべきことはいろいろありそうですが、詳しくはシェルリらの論文(Traits: Composable Units of Behaviour など)を読んでいただくとして、ここでは実装の多重継承のしくみとして考えた場合のミクスイン機構との違いを、少々乱暴ですが単純に、ミクスインの「リニア化」かトレイトの「フラット化」か、に絞ることが可能だと考えることにします。

例えば、複数のミクスインM1、M2(おのおのがさらにM0を継承)を同時に継承した場合、ミクスイン機構では何らかのルールに従ってミクスインを順番に列べ(リニア化し)て、その列びを継承パスに挿入します。Pythonで書くとこんな感じでしょうか。

class B(object):
    def m(self):
        print "B", 

class C(B):
    def m0(self):
        print "C", 
        self.m()

C().m0()  #=> C B
class M0(object):
    def m(self):
       print "M0", 
       super(M0, self).m()

class M1(M0):
    def m(self):
       print "M1", 
       super(M1, self).m()

class M2(M0):
    def m(self):
       print "M2", 
       super(M2, self).m()

class C(M1, M2, B):
    def m0(self):
        print "C", 
        self.m()

C().m0()  #=> C M1 M2 M0 B


こうしたミクスインの挙動に対し、トレイト機構では同時に use するトレイトはまず「フラット化」されます。トレイトは、とりあえずメソッドを束ねたもの(集合)だと思ってください。同時に use される複数のトレイトは、いったんばらしてひとつの仮想的なトレイトにまとめられて、その後、改めてクラスに use される(あるいは「注入」される)と考えると少しわかりやすいです。その際、メソッド名の衝突が見つかれば実行時にエラーになったりコンパイルに失敗します。つまり、先の Python のミクスインの例のようなコードはトレイトではこのままでは動きません。

実際にどうなるか Squeak Smalltalk で試してみましょう。

次のコードは、先のコードのミクスインのところをトレイトに置き換えて Squeak Smalltalk 向けに書き直したものです。Squeak Smalltalk の IDE にあまり詳しくなくてもただ起動してワークスペースなどに貼り付け、空行で隔てられた式ひとつひとつを順に選択してから do it (alt/cmd + d)するか、fileIn (全体を選択してから alt/cmd + shift + g。もしくはこのコードを example.st に保存しておき (FileStream fileNamed: 'example.st') fileIn を do it する等々)して実行できるコードとして記述してあります。クラスブラウザを使わないため、通常の Smalltalk のコードの書き方や実行のしかたからは外れますが、その点ご留意ください。

なお、同様のことはドットインストールの「Smalltalk入門」で話題の Pharo Smalltalk でも試せますが、Pharo 1.4 には #uses: が無いので自分で別途定義しておくか、該当メソッドを使用している式をあらかじめ書き換える必要があります(ちょっと長いですが B subclass: #C uses: T1-{#m}+T2 instanceVariableNames: '' classVariableNames: '' poolDictionaries: '' category: 'Traits-Examples'. などとして試してみてください)。

Object subclass: #B
   instanceVariableNames: ''
   classVariableNames: ''
   poolDictionaries: ''
   category: 'Traits-Examples'.

B compile: 'm
   Transcript space; show: #B; cr; endEntry'.

B subclass: #C
   instanceVariableNames: ''
   classVariableNames: ''
   poolDictionaries: ''
   category: 'Traits-Examples'.

C compile: 'm0
   Transcript space; show: #C.
   self m'.

Transcript open.

C new m0.  "=> C B "
Trait named: #T0 uses: {} category: 'Traits-Examples'.

T0 compile: 'm
   Transcript space; show: #T0.
   super m'.

Trait named: #T1 uses: T0 category: 'Traits-Examples'.

T1 compile: 'm
   Transcript space; show: #T1.
   super m'.

Trait named: #T2 uses: T0 category: 'Traits-Examples'.

T2 compile: 'm
   Transcript space; show: #T2.
   super m'.

C uses: T1 + T2.

Transcript open.

C new m0  "=> Error: A class or trait does not properly resolve a conflict between multiple traits it uses. "


このように衝突の検出を告げるエラーになってそのままでは最後の出力ができません。

検出された衝突を回避するには、原因となっているメソッドのうちどれをどのトレイトから排除するかを use するときに明示的にしてやる必要があります。たとえば、エラーを出す最後の式を含む三つの式を次のような式に置き換えると正常に動作させられます(この場合は、トレイトT1 のメソッドm を排除することで、結果的にトレイトT2 の m を残しています)。

C uses: T1 - {#m} + T2.

Transcript clear.

C new m0  "=> C T2 B "


出力結果もミクスインの場合とは違っていることに気付かれたでしょうか。このように出力されるとまるで T0 や T1 ごと排除されて機能していないように見えますが、実際にはそんなことはありません。あくまでメソッド m についてのみ、フラット化の結果、T2 由来の m が使われているというだけの話です。T0 や T1 も引き続き use されていますので、例えば、T0 にメソッドmT0、T1 に mT1 を追加で定義してやれば、ふつうにそれぞれを C のインスタンスからコールできます。

T0 compile: 'mT0
   Transcript space; show: #mT0'.

T1 compile: 'mT1
   Transcript space; show: #mT1'.

C new mT0; mT1.  "=> mT0 mT1 "


これが、本来、トレイトに期待される挙動です。

では、Scala のトレイトはどうかというと、話の流れからもう予想は容易につくと思いますが、こんなふうになります。

trait TA {
   def m() : Unit
}

trait T0 extends TA {
   abstract override def m  = {
      print("T0 ")
      super.m
   }
}

trait T1 extends T0 {
   abstract override def m = {
      print("T1 ")
      super.m
   }
}

trait T2 extends T0 {
   abstract override def m = {
      print("T2 ")
      super.m
   }
}

class B {
   def m = println("B")
}

class C1 extends B {
   def m0 = {
      print("C1 ")
      m
   }
}

class C2 extends B with T1 with T2 {

   override def m = super.m

   def m0 = {
      print("C2 ")
      m
   }

}

object Example extends App {
   (new C1).m0  //=> C1 B
   (new C2).m0  //=> C2 T2 T1 T0 B
}


Python と同じミクスインの挙動ですね。あと、あいにく宣言時に何も extends していない T0 の m からは super.m がコールできないため、トレイトを使った後者の出力では B が抜けています。

追記:B の m が T0 の m から super.m でコール出来ない件は abstract override 修飾子と抽象トレイトを一段噛ますことで実現できるようです。

REPL の :power モードで確認すると、Python のときと同様に、C2 の継承パスの T0 のすぐ上流に B がきちんと挿入されていることが分かります(なぜか先の実行の結果と T1 T2 が入れかわってしまっていますが???)。

追記: この件、@kmizu さんから次のようなご指摘をいただきました。ありがとうございます。

ScalaのClass Linerizationの仕様については、 http://www.scala-lang.org/docu/files/ScalaReference.pdf の p.56 に記載があるので、そっちを参照されるのが良いかと。REPLのpower modeではClass Linearizationの結果は(簡単には)取れないと思います。

— Kota Mizushimaさん (@kmizu) 2013å¹´3月7æ—¥

scala> :power
** Power User mode enabled - BEEP WHIR GYVE **
** :phase has been set to 'typer'.          **
** scala.tools.nsc._ has been imported      **
** global._, definitions._ also imported    **
** Try  :help, :vals, power.<tab>           **

scala> intp.types("C2").tpe.baseTypeSeq.toList
res1: List[$r.intp.global.Type] = List(C2, T1, T2, T0, B, Object, Any)


だから何だ?なんか文句があるのか?とかつっかかられたりすると困りますが、Scala のトレイトはトレイトじゃないからシェルリらのトレイトについてほとんど知らなくても問題ないよ、あと Scala のトレイトの挙動を前提にシェルリらのトレイトの説明を読むと混乱するから気をつけて、といった老婆心から書いてみました。



追記:
PHP も 5.4.0 からシェルリらのトレイトをサポートしていますので、同様のサンプルコードを書いてみました。当たり前ですが Squeak Smalltalk のトレイトと同じ挙動をします。

<?php
trait T0 {
   public function m() {
      echo "T0 ";
      parent::m();
   }
}

trait T1 {
   use T0; 
   public function m() {
      echo "T1 ";
      parent::m();
   }
}

trait T2 {
   use T0;
   public function m() {
      echo "T2 ";
      parent::m();
   }
}

class B {
   public function m() {
      echo "B\n";
   }
}

class C0 extends B {
   public function m0() {
      echo "C0 ";
      $this->m();
   }
}

/*
class C1 extends B {
   use T1, T2;  //=> Fatal error: Trait method m has not been applied, because there are collisions with other trait methods
   public function m0() {
      echo "C1 ";
      $this->m();
   }
} // */

class C2 extends B {
   use T1, T2 { T2::m insteadof T1; }
   public function m0() {
      echo "C2 ";
      $this->m();
   }
}

(new C0())->m0();  //=> C0 B
(new C2())->m0();  //=> C2 T2 B