Smalltalk・PHPのトレイトとRuby・Scalaのミックスインの違い

  • 19
    Like
  • 9
    Comment

シェルリ(Schärli)が発案して Smalltalk でその実効を試したトレイト(Traits。エンティティとしては trait)はちょっとわかりにくい言語機能なので Ruby のモジュールや Scala のトレイト(これまた紛らわしい名前…)に代表されるミックスインと比較してみましょう。

まず、ミックスインというのは Ruby で話題にはなりましたが特に新しい機構というわけではなく、むしろもっとも古く、オーソドックスな多重継承機構です。クラスまたはクラスに準ずるエンティティ(Ruby ならモジュール、Scala ならトレイト(名称詐欺!)、抽象クラスなど、インスタンス生成能を持たないクラス様のもの)を継承パスに任意の数を挿入できる機構がミックスインです。

それに対して、トレイトは継承機構とはまったく関係ないところで機能します。端的には、本来であればクラスの一部であるメソッド辞書(あるいはもっと抽象的にメソッドの集合)を取り出してエンティティにしたのがトレイトです。クラスに組み込んで使用(use)します。

この性質の違いは、ミックスイン、トレイト、それぞれでスーパークラスのメソッドをコールする仕組みである super(PHP では parent)を使ってみたときにはっきりします。

module M0
  def m; super; print "M0#m " end
end

module M1
  include M0
  def m; super; print "M1#m\n" end
end

class B
  def m; print "B#m " end
end

class C < B
  include M1
end

C.new.m  #=> B#m M0#m M1#m
trait TA {
  def m : Unit
}

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

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

class B {
  def m = print("B::m ")
}

class C extends B with T1 {
  override def m = super.m
}

object Main extends App { (new C).m } //=> B::m T0::m T1::m 
| T0 T1 B |
T0 := Trait named: #T0 uses: {} category: 'UserObjects'.
T0 compile: 'm ^super m, ''T0>>#m '''.

T1 := Trait named: #T1 uses: T0 category: 'UserObjects'.
T1 compile: 'm ^super m, ''T1>>#m'''.

B := Object newSubclass rename: #B.
B compile: 'm ^''B>>#m '''.

(B newSubclass rename: #C) uses: T1.

C new m  "=> 'B>>#m T1>>#m' "

{C. B. T0. T1} do: #removeFromSystem.
<?php

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

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

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

class C extends B {
   use T1;
}

(new C())->m(); //=> B::m T1::m

Ruby と Scala がミックスイン、Smalltalk と PHP が(シェルリの)トレイトです。

前者(ミックスイン)は継承パスに参加しているので、線形化後にパスの上位に配置されたモジュールなどはスーパークラス(基底クラス)と同様に見なされ、そこに定義されたメソッドは下位に位置する(すなわちミックスイン先の)クラスや別のミックスインから super でコール可能です。

対して後者(トレイト)は use されたクラスに組み込まれるので、下位クラスの super(あるいは parent)の挙動には影響を及ぼしません(ただし、シェルリのトレイトの特性として、トレイトを use する際、use する側のクラスまたはトレイトに同名メソッドがあるとコンフリクトとは扱われずに単に遮蔽されるという挙動をします)。

この話とは別に、トレイトでは複数同時に use されたときにフラット化をしてコンフリクトを検出するのに対し、Ruby など動的型言語のミックスインでは単なるオーバーライドでコンフリクトとは見なされず、それを検知する特別なしくみはない(Scala のような静的型言語にはミックスインに限らずオーバーライドするときは override修飾子で明示的にする制約を設けて、それを利用してコンフリクトを副次的に排除することはありますが… 普通に備わっているそうです)という違いもあります。