RubyTraits-0.2 であそぶ


Matzにっき 経由で、RubyForge: RubyTraits


Traits というのは、 Black、Schärli らにより 2002 年頃に提唱され、Squeak Smalltalk を使って実験的な実装が行なわれていた新しい多重継承機構やそのためのエンティティ(trait)のことです。Squeak Smalltalk では 3.9 から組み込みになりました。RubyTraits は、この Squeak Smalltalk の Traits の Ruby 版です。なお、ここで多重継承は広い意味で、以下でミックスインは狭い意味で使っています。念のため。


まず、Traits について。最近わかってきたのですが、どうやら Traits は、それを備えた言語が動的か、静的か、で説明の仕方やそこから受ける印象がかなり変わるようです。SmalltalkPerl6(Roles)追記:Perl6 の Roles は、説明や実装はどちらかというと後者寄り…?)Ruby の場合、トレイトはクラスなどに脱着可能なメソッドの集合と紹介され、コンフリクトを回避するためのトレイト間の合成機能がフィーチャーされます。このとき Traits は、従来のクラス(や、Ruby のモジュールを含む擬似的なクラス)を用いたミックスイン機構が抱える問題を克服するために考え出された新しい機能であるといった印象をうけます。

他方で、FortressScala では、トレイトはメソッドを含めることができるインターフェイスのようなものであると説明されます。これだと、対抗馬というよりはまるで静的言語向けの Ruby のモジュール様機構そのものであるような印象を受けます。ミックスインに対して云々よりは、クラスやインターフェイスの役回りの一部を肩代わりする新顔といったふうです。実際、静的言語での Traits の実装では動的言語向けにはない継承パスへ参加する振る舞いがあったり、トレイト同士の合成機能を欠いているなど、Ruby のモジュールとも区別をつけにくくなっているように感じます。

あと関係ないのですが、そもそも「trait」という言葉自体、「特性、特色、特徴」という意味を持つ都合、古くは XEROX の Star システムの多重継承機構、C++ のイディオム、比較的歴史の近いところでも SELF のクラスに当たるオブジェクトの呼称(trait)などに使われていてまぎらわしいので、この点でも注意が必要です。


さて本題。RubyTraits 0.2 では pure-traits と ruby-traits の二つの実装が提供されています。前者は Schärli らのオリジナルの Traits の振る舞いを模したもの。後者は Ruby ユーザーのメンタルモデルに合わせて Traits の仕様のいくつかを改変した(つまりモジュールっぽい挙動をさせる)もののもよう。まずは Squeak Smalltalk で使い方が分かっている前者を試します。


追記(注意):
以下では簡単のためトレイトやそれを use したクラスの定義式は「Trait named: #T」、「Object subclass: #C uses: T」のように略記していますのでそのままでは実行できません。実際にトレイト T を定義するためには「Trait named: #T uses: {} category: 'Category-Name'」、また、トレイト T を use したクラス C の定義には「Object subclass: #C uses: T instanceVariableNames: '' classVariableNames: '' poolDictionaries: '' category: 'Category-Name'」といったような式をどこか(通常はワークスペースですが、Squeak環境の場合は文字が入力できる場所であればどこでもOK)に入力して選択後(独立行なら選択は不要)、do it (alt/cmd + d) する必要があります。
また、「クラス名もしくはトレイト名>>」というのは、そのメソッドが定義された場所を示すための便宜的な記法で Smalltalk の構文ではありません。「>>」より後がそのメソッドのソースになるので「>>」直後より後のメソッドの定義コード部分を選択してコピーし、ブラウザにペーストしコンパイルするなどして定義してください。クラスブラウザやその他の便利ツール群の使い方は「自由自在 Squeakプログラミング」などのチュートリアルを一読されることを強くお勧めします。クラスブラウザを使いたくない場合は、メソッド定義コード部分を文字列引数にして与えた「T compile: 'hoge ^''T#hoge'''」などといった式を do it (alt/cmd + d) することで定義できます。その際、コード中のシングルクオートはエスケープ(シングルクオートを二つ重ねる)する必要がある点にも注意してください。なお初回のコンパイル時、イニシャルを要求されることがありますので適当に与えてやってください。言い訳ですが、この手の情報はここに書き始めると長くなるのでこのブログでは省略していることがよくあります。分かりづらくてすみません。^^;


▼メソッド #hoge を定義したトレイト T をクラス C で use

Sqeuak Smalltalk
Trait named: #T

T >> hoge
    ^'T#hoge'

Object subclass: #C
    uses: T
C new hoge   "=> T#hoge "
C >> hoge
    ^'C#hoge'
C new hoge   "=> C#hoge -- 同名メソッドがクラスで定義された場合、トレイトの定義は遮蔽される "
C >> hoge
    ^super hoge
C new hoge   "=> MessageNotUnderstood: Object>>hoge -- トレイトは継承パスに参加していない "
C withAllSuperclasses asArray  "=> {C . Object . ProtoObject} "
Ruby(pure-traits)
require "pure-traits"

T = trait{ def hoge; "T#hoge" end }
class C; use T end

C.new.hoge   #=> T#hoge

class C; def hoge; "C#hoge" end end

C.new.hoge   #=> C#hoge

class C; def hoge; super end end

C.new.hoge   #=> NoMethodError: super: no superclass method `hoge'
C.ancestors   #=> [C, Object, Kernel]


この例では最初、クラス C にメソッド #hoge は定義されていませんが、#hoge を定義したトレイト T を use することで C のインスタンスは #hoge をコールできるようになります。このような振る舞いは、#hoge を定義した Ruby のモジュールを include したときと一緒ですね。


ここであらためて C に #hoge を定義したとき、まずは両者に違いが生じます。モジュール使用時における同様の場合と異なり、トレイト T の #hoge は、クラス C に定義された #hoge により遮蔽され、通常の方法(たとえば super へのメッセージ送信。Ruby では super のコール)ではアクセスできなくなります。こうした違いは、両者における、クラスへのメソッドの提供様式の違いに起因するようです。モジュールが、それをインクルードするクラスの擬似的なスーパークラスになることで自らに定義されたメソッドを提供するのに対し、トレイトは、提供するメソッド群について、まるでそれらが(本当は定義されていないにもかかわらず)最初から定義してあったかのようにクラスに振る舞わせようとします。したがって、C に #hoge がちゃんと定義されたあかつきには、T が提供した #hoge はお役ご免になる…といった感じでしょうか。



Traits と Ruby のモジュールとのさらなる違いは、多重継承時にメソッドの衝突が複数起こったときに際だちます。


▼いずれも #hoge と #fuga を持つ T1、T2 両方を use したときの C の振る舞い

Squeak Smalltalk
Trait named: #T1

T1 >> hoge
    ^'T1#hoge'

T1 >> fuga
    ^'T1#fuga'

Trait named: #T2

T1 >> hoge
    ^'T2#hoge'

T2 >> fuga
    ^'T2#fuga'

Object subclass: #C
    uses: T1 + T2
C new hoge   "=> Error: A class or trait does not properly resolve a conflict between multiple traits it uses. "
Object subclass: #C
    uses: T1 - {#hoge} + T2 - {#fuga}
C new hoge   "=> 'T2#hoge' "
C new fuga   "=> 'T1#fuga' "
Ruby(pure-traits)
require "pure-traits"

T1 = trait{
  def hoge; "T1#hoge" end
  def fuga; "T1#fuga" end
}

T2 = trait{
  def hoge; "T2#hoge" end
  def fuga; "T2#fuga" end
}

class C; use T1, T2 end   #=> ["fuga", "hoge"] -- 衝突のレポート

C.new.hoge   #=> TraitsConflict: hoge
C.new.fuga   #=> TraitsConflict: fuga

class C; use (T1 - :hoge) + (T2 - :fuga) end   #=> []

C.new.hoge   #=> T2#hoge
C.new.fuga   #=> T1#fuga


Ruby のモジュールをはじめとする、擬似的なクラス(もしくはクラスそのもの)を用いたミックスイン機構では、衝突したメソッド群のうちどのメソッドを優先するかについて衝突ごとに管理する専用のしくみがありません。Traits では、複数のトレイトはいったん合成してからクラスに use されるため、この時点で衝突を判定でき、各々の衝突をどのように回避するかについて策を講じることが可能になっています。




他方で、ruby-traits のほうでは、トレイトも継承パスに参加するところが pure-traits と違うようです。

▼メソッド #hoge を定義したトレイト T をクラス C で use

require "ruby-traits"

T = trait{ def hoge; "T#hoge" end }
class C; use T end

C.new.hoge   #=> T#hoge

class C; def hoge; "C#hoge" end end

C.new.hoge   #=> C#hoge

class C; def hoge; super end end

C.new.hoge   #=> T#Hoge
C.ancestors   #=> [C, Trait@080161cc {"hoge"=>134308300}, Object, Kernel]

▼いずれも #hoge と #fuga を持つ T1、T2 両方を use したときの C の振る舞い

require "ruby-traits"

T1 = trait{
  def hoge; "T1#hoge" end
  def fuga; "T1#fuga" end
}

T2 = trait{
  def hoge; "T2#hoge" end
  def fuga; "T2#fuga" end
}

class C; use T1, T2 end   #=> C -- 衝突のレポートがない

C.new.hoge   #=> TraitsConflict: hoge
C.new.fuga   #=> TraitsConflict: fuga

class C; use (T1 - :hoge) + (T2 - :fuga) end   #=> C

C.new.hoge   #=> T2#hoge
C.new.fuga   #=> T1#fuga

念のため、Ruby のモジュールの場合。

module M1
  def hoge; "M1#hoge" end
  def fuga; "M1#fuga" end
end

module M2
  def hoge; "M2#hoge" end
  def fuga; "M2#fuga" end
end

class C1; include M1, M2 end
C1.ancestors   #=> [C1, M1, M2, Object, Kernel]

class C2; include M2, M1 end
C2.ancestors   #=> [C2, M2, M1, Object, Kernel]

C1.new.hoge   #=> M1#hoge
C1.new.fuga   #=> M1#fuga
C2.new.hoge   #=> M2#hoge
C2.new.fuga   #=> M2#fuga

class C
  include M2
  alias :_hoge :hoge
  include M1
  alias :hoge :_hoge
end
C.ancestors   #=> [C, M1, M2, Object, Kernel]

C.new.hoge   #=> M2#hoge
C.new.fuga   #=> M1#fuga


このように、衝突が小規模ならモジュールのインクルードのタイミングと別名機能(alias)を組み合わせることで、今の Ruby のモジュールでもある程度の対処は可能です。