以下の記事は2010年のものですが、モジュールに後から別のモジュールを動的にincludeした場合の挙動について解説されていたのが気になったので追ってみました。
参考: Rubyのモジュール機構では、既にincludeしてるモジュールに追加でincludeしても結果が反映されない - Smalltalkのtは小文字です
当時のMatzのツイートも引用されていましたが、スレッドを追えなくなってて元がわからない状態でした。
@yhara いや、既にincludeしてるモジュールに追加でincludeすると、追加が反映されず継承木の構造が期待と違う、という意味です。
— Yukihiro Matsumoto (@yukihiro_matz) November 14, 2010
Ruby 2.5.3で動かしてみた
以下はirbで実行する前提です。
なお、Ruby 2.6.0でも同じ結果でした。
1. 普通のモジュールincludeの挙動
module Foo
def foo
"I'm Foo's foo!"
end
end
class Bar
include Foo
end
Bar.new.foo #=> "I'm Foo's foo!"
Bar.new.class.ancestors #=> [Bar, Foo, Object, Kernel, BasicObject]
これは普通の挙動ですね。
2. includeしたモジュールに後から別のモジュールをincludeした場合
上に続けて、(モジュールFooの#fooの挙動を変えようと思ったなどの理由で)、クラス定義の「後から」モジュールFooにモジュールBazをincludeしたとします。
module Baz
def foo
"I'm Baz's foo!"
end
def baz
"I'm Baz's baz!"
end
end
Foo.include Baz # 後からモジュールFooにモジュールBazをinclude
Bar.new.foo #=> "I'm Foo's foo!"
Bar.new.baz #=> NoMethodError (undefined method `baz' for #<Bar:0x00007feadb9d29e8>)
Bar.new.class.ancestors #=> [Bar, Foo, Object, Kernel, BasicObject]
確かに、モジュールBazの#fooは反映されませんでした😳。継承パスを見るとそもそもBazはincludeされていません。
後からだと、モジュールBazに何を書いてもincludeされないので、Baz#bazは動きません。
この動作は上の記事が書かれた頃から変わっていないことがわかりました。
Railsで遭遇することはなさそうですが、irbでインタラクティブにあれこれ試しているときにたまにハマるかもしれません。
includeの後でクラスを定義すれば通常動作になる
ただし、Foo.include Bazより後で行うクラス定義ではBazがincludeされます。上に続けて以下を実行します。
class Boo
include Foo
end
Boo.new.foo #=> "I'm Foo's foo!"
Boo.new.baz #=> "I'm Baz's baz!"
Boo.new.class.ancestors #=> [Boo, Foo, Baz, Object, Kernel, BasicObject]
普通の動作ですね。その代り継承順序によって、Baz#fooではなくFoo#fooが実行されます。
3. 同じことをprependでやってみた場合
ここでirbを再起動します。ふと気になったので、試しにモジュールFooで今度はprepend Bazしてみました。
module Foo
def foo
"I'm Foo's foo!"
end
end
class Bar
include Foo
end
module Baz
def foo
"I'm Baz's foo!"
end
def baz
"I'm Baz's baz!"
end
end
Foo.prepend Baz # 後からモジュールFooにモジュールBazをprepend
Bar.new.foo #=> "I'm Foo's foo!"
Bar.new.baz #=> NoMethodError (undefined method `baz' for #<Bar:0x00007fdc3a83b3a0>)
Bar.new.class.ancestors #=> [Bar, Foo, Object, Kernel, BasicObject]
やはり、後からモジュールFooでモジュールBazをprependしても効きません。さっきのモジュールincludeのチェインでincludeが効かなかったのと同じメカニズムなのだろうと推測しています。
prependの後でクラスを定義すれば通常動作になる
上に続けて以下を実行した場合も、includeの場合とまったく同じです。
class Boo
include Foo
end
Boo.new.foo #=> "I'm Foo's foo!"
Boo.new.baz #=> "I'm Baz's baz!"
Boo.new.class.ancestors #=> [Boo, Foo, Baz, Object, Kernel, BasicObject]
4. モジュールBazをクラス側で普通にincludeした場合
念のためirbをいったんexitして再度起動し、以下を実行しました。今度はモジュールFooでのincludeは行いません。
module Foo
def foo
"I'm Foo's foo!"
end
end
module Baz
def foo
"I'm Baz's foo!"
end
def baz
"I'm Baz's baz!"
end
end
class Beh
include Foo
include Baz
end
Beh.new.foo #=> "I'm Baz's foo!"
Beh.new.baz #=> "I'm Baz's baz!"
Beh.new.class.ancestors #=> [Beh, Baz, Foo, Object, Kernel, BasicObject]
今度は期待どおりモジュールBazのincludeが効きました☺。継承順序がさっきと逆になり、Foo#fooではなくBaz#fooが実行されます。普通の挙動ですね。
補足: prependについて
上の元記事が出た2010年にはRubyにprependはまだ登場していなかったと思います。以下によると、元記事はRuby 1.9.3が出た直後でした。
参考: Ruby Releases
TechRachoの記事では、2013年にRuby 2.0.0でprependが導入されたとあります↓。
なお、irbを再起動してから以下のようにクラスにモジュールBazをprependすると、さすがに継承パスのトップに配置されました。
module Foo
def foo
"I'm Foo's foo!"
end
end
module Baz
def foo
"I'm Baz's foo!"
end
def baz
"I'm Baz's baz!"
end
end
class Bar
include Foo
end
Bar.prepend Baz
Bar.new.foo #=> "I'm Baz's foo!"
Bar.new.baz #=> "I'm Baz's baz!"
Bar.new.class.ancestors #=> [Baz, Bar, Foo, Object, Kernel, BasicObject]
ではGobyではどうか
せっかくなので、冒頭のコードを愛しのGobyのREPLでも試してみました。私は普段からRubyとGobyで同じコードを実行して比べています。
$ goby -i
Goby 0.1.10 😉 😂 🤠
» module Foo
¤ def foo
¤ "I'm Foo's foo!"
¤ end
» end
#»
»
» class Bar
¤ include Foo
» end
#» Bar
»
» Bar.new.foo
#» I'm Foo's foo!
» Bar.new.class.ancestors
#» [Bar, Foo, Object]
» module Baz
¤ def foo
¤ "I'm Baz's foo!"
¤ end
¤
¤ def baz
¤ "I'm Baz's baz!"
¤ end
» end
#»
»
» Foo.include Baz
#» Foo
» Bar.new.foo
#» I'm Foo's foo!
» Bar.new.baz
#» I'm Baz's baz! # エラーにならない
» Bar.new.class.ancestors
#» [Bar, Foo, Baz, Object]
というわけでGobyでは、include済みのモジュールに別のモジュールをincludeできました。おそらく狙った動作ではなく、たまたまだと思います。Gobyではincludeするモジュールをコピーではなく参照にしているのかなと推測しています。