Ruby 3.0 で変わる Module#include の挙動
今日の RubyKaigi Takeout 2020 の Ruby Committers vs the World でチラッと話が出たのでちょっとまとめでみました。
注意!!
これはまだ開発版の機能になり Ruby 3.0 がリリースされるタイミングで挙動が変わっている可能性があるので注意してください。
Module#include
の変更点
今までは M.include M2
を行ってもすでに include M
しているクラス/モジュールの継承リストには反映されませんでした。
module M; end class X include M end # ここでは M のみ反映されている p X.ancestors # => [X, M, Object, Kernel, BasicObject] module M2; end # M に対して include してもすでに M を include しているクラス/モジュールの継承リストには反映されない M.include M2 # M2 は反映されない p X.ancestors # => [X, M, Object, Kernel, BasicObject]
これが Ruby 3.0 からではすでに include
されているクラス/モジュールにも反映されるようになります。
module M; end class X include M end # ここでは M のみ反映されている p X.ancestors # => [X, M, Object, Kernel, BasicObject] module M2; end # M に対して include すると M を include しているクラス/モジュールにも反映される M.include M2 # M2 も反映されるようになる p X.ancestors # => [X, M, M2, Object, Kernel, BasicObject]
これは prepend
でも同じ挙動になります。
また、これにより(当然ですが)メソッド呼び出しにも影響はあります。
module M1 def hoge; end end class X include M1 end # OK X.new.hoge module M2 def foo; end end M1.include M2 # 2.7 : error # 2.8 : OK X.new.foo
この変更自体は結構うれしくて、例えば汎用的なモジュールをあとから Kernel
に対して include
できるようになります。
module M def twice self + self end end Kernel.include M p "hoge".twice # 2.7 => error: undefined method `twice' for "hoge":String (NoMethodError) # 3.0 => "hogehoge"
これは便利。
Module#include
の変更に対する弊害
弊害というか期待する挙動ではあるんですが以下のようなケースの場合、継承リストに同じモジュールが複数含まれるようになります。
class Super def hoge ["Super#hoge"] end end module Included def hoge ["Included#hoge"] + super end end module Prepended def hoge ["Prepended#hoge"] + super end end class X < Super include Included prepend Prepended def hoge ["X#hoge"] + super end end # このあたりは今までどおり期待する挙動 p X.ancestors # => [Prepended, X, Included, Super, Object, Kernel, BasicObject] p X.new.hoge # => ["Prepended#hoge", "X#hoge", "Included#hoge", "Super#hoge"] module Included2 def hoge ["Included2#hoge"] + super end end # Ruby 3.0 の変更であとから include した場合でも反映されるようになる Prepended.include Included2 Included.include Prepended # Ruby 3.0 だと同じモジュールが複数含まれるようになる… p X.ancestors # => [Prepended, Included2, X, Included, Prepended, Included2, Super, Object, Kernel, BasicObject] # 当然 super 経由で同じめそっどが 複数回呼ばれるようになる p X.new.hoge # => ["Prepended#hoge", "Included2#hoge", "X#hoge", "Included#hoge", "Prepended#hoge", "Included2#hoge", "Super#hoge"]
これは期待する挙動ではあるんですが今までの挙動を知っていると結構ギョッとする挙動になりますねえ。
実際、これに関するバグ報告も来ていました(期待する挙動なのですでに閉じられています。
と、言う感じで Ruby 3.0 では Module#include
の挙動が変わる(かもしれない)ので注意してください。