Tech Racho エンジニアの「?」を「!」に。
  • 開発

Ruby: includeしたモジュールに「後から」別のモジュールを動的にincludeしても反映されない

以下の記事は2010年のものですが、モジュールに後から別のモジュールを動的にincludeした場合の挙動について解説されていたのが気になったので追ってみました。

参考: Rubyのモジュール機構では、既にincludeしてるモジュールに追加でincludeしても結果が反映されない - Smalltalkのtは小文字です

当時のMatzのツイートも引用されていましたが、スレッドを追えなくなってて元がわからない状態でした。

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でモジュールBazprependしても効きません。さっきのモジュール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が導入されたとあります↓。

Ruby 2.0.0リリース! – prependを使ってみよう

なお、irbを再起動してから以下のようにクラスにモジュールBazprependすると、さすがに継承パスのトップに配置されました。

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

参考記事

Goby: Rubyライクな言語(2)Goby言語の全貌を一発で理解できる解説スライドを公開しました!


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。