ブログ記事からの転載です。
今日の 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, X, Included, 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
の挙動が変わる(かもしれない)ので注意してください。