- Ruby / Rails以外の開発一般
READ MORE
以下の記事は2010年のものですが、モジュールに後から別のモジュールを動的にincludeした場合の挙動について解説されていたのが気になったので追ってみました。
参考: Rubyのモジュール機構では、既にincludeしてるモジュールに追加でincludeしても結果が反映されない - Smalltalkのtは小文字です
当時のMatzのツイートも引用されていましたが、スレッドを追えなくなってて元がわからない状態でした。
@yhara いや、既にincludeしてるモジュールに追加でincludeすると、追加が反映されず継承木の構造が期待と違う、という意味です。
— Yukihiro Matsumoto (@yukihiro_matz) November 14, 2010
以下はirbで実行する前提です。
なお、Ruby 2.6.0でも同じ結果でした。
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]
これは普通の挙動ですね。
上に続けて、(モジュール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でインタラクティブにあれこれ試しているときにたまにハマるかもしれません。
ただし、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
が実行されます。
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
が効かなかったのと同じメカニズムなのだろうと推測しています。
上に続けて以下を実行した場合も、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
をクラス側で普通に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の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するモジュールをコピーではなく参照にしているのかなと推測しています。