以下の記事は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するモジュールをコピーではなく参照にしているのかなと推測しています。