SmalltalkとRubyの動的変更への追従性対決: トレイト vs モジュール

  • 6
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

もちろん出来レースです。Ruby には負けてもらいます。w


Ruby ではモジュールを、それを継承させたいクラスに include することでミックスインできます。

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

class C
  include M1
end

C.new.m1   #=> M1#m1

オープンクラス機構を有する Ruby は include したモジュールにメソッドを後から追加しても、その変更は即座に反映されます。さすが Ruby 。すごく動的ですね。

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

C.new.m2   #=> M1#m2

モジュールには別のモジュールもミックスインできます。当然、その結果はすでに当該モジュールを include 済みのクラスやインスタンスにも反映されるはず……ん?

module M2
  def m3; p "M2#m3" end
end

module M1
  include M2
end

C.new.m3  #=> NoMethodError: undefined method `m3'

なのですが、どうやらそうはならないみたいですね。

なぜか Ruby のモジュールは、いったん include すると、それに別のモジュールを include してもその結果は既に include 済みのクラス等に対しては反映されない仕様になっている模様。

反映させるためには改めて include してやる必要があるみたいです。

class C
  include M1
end

C.new.m1  #=> M1#m1
C.new.m2  #=> M1#m2
C.new.m3  #=> M2#m3


対して Squeak Smalltalk のトレイトも、クラスに use されるのはあくまでその時点でのメソッド群で、その後 use したトレイトに変更があっても追従できるしくみになっているようには見えません。

ClassDescription >> installTraitsFrom: aTraitComposition
    
"Install the traits from the given composition. This method implements
    the core composition method - all others are just optimizations for
    particular cases. Consequently, the optimized versions can always fall
    back to this method when things get too hairy."

    
| allTraits methods |

    
(self traitComposition isEmpty and: [aTraitComposition isEmpty]) ifTrue: [^self].

    
"Check for cycles"
    
allTraits := aTraitComposition gather: [:t | t allTraits copyWith: t].
    
(allTraits includes: self) ifTrue:[^self error: 'Cyclic trait definition detected'].

    
self traitComposition: aTraitComposition.
    
methods := self assembleTraitMethodsFrom: aTraitComposition.
    
self installTraitMethodDict: methods.
    
self isMeta ifFalse:[self classSide updateTraitsFrom: aTraitComposition].


がしかし、実際に変更を加えてみると…

| T1 C T2 |
T1 := Trait named: #T1 uses: {} category: 'UserObjects'.
T1 compile: 'm1 ^''T1>>#m1'''.

C := Object newSubclass uses: T1; rename: #C.
C new m1.  "=> 'T1>>#m1 ' "

T1 compile: 'm2 ^''T1>>#m2'''.
C new m2.  "=> 'T1>>#m2' "

T2 := Trait named: #T2 uses: {} category: 'UserObjects'.
T2 compile: 'm3 ^''T2>>#m3'''.
T1 uses: T2.

C new m3.  "=> 'T2>>#m3' "

{C. T1. T2} do: #removeFromSystem "for clean up"

と、普通に動的に反映してくれます。勝負ありましたね(出来レース乙)。

もちろん Smalltalk のことですから、泥臭く一生懸命差し替えることで、この動的性を実現していることは言うまでもありません。

updateTraits.png

実にけなげですね。