今更聞けない! Ruby の継承と mixin の概念を継承リストから学ぶ
Ruby を学ぶ上で継承と mixin の概念を理解することはとても重要である。
しかし、このあたりの仕組みを学ぼうとすると、include
や prepend
、特異クラスや特異メソッドなどという様々な機能を理解する必要がありとても複雑である。
そこで本記事は『継承リスト』という観点から継承を mixin を学んでみたいと思う。
また、この記事は Ruby 2.4 時点での仕様になる。
継承と mixin
まず、はじめに Ruby での継承と mixin の仕方をみてみよう。
Ruby では継承で既存のクラスを、mixin でモジュールをそれぞれクラスに取り込むことが出来る
継承
class Super def super_method :super_method end end # Super クラスの機能(メソッド)を Sub クラスで継承(取り込む)する class Sub < Super end # Super クラスのメソッドが利用できる p Sub.new.super_method # => :super_method
mixin
module Mod def mod_method :mod_method end end class X # Mod モジュールの機能(メソッド)を X クラスで取り込む include Mod end # Mod モジュールのメソッドが利用できる p X.new.mod_method # => :mod_method
他にも #extend
や #prepend
といったメソッドでモジュールを mixin することが出来る。
継承も mixin も『外部の機能をクラスに取り込む』という意味では共通だが、
- 1つのクラスしか継承する事が出来ない(継承してるクラスを継承するのは可)
- 複数のモジュールを mixin することが出来る
という点で少し異なる。
また、継承と mixin は同時に利用することも可能である。
class Super def super_method :super_method end end module Mod def mod_method :mod_method end end class X < Super include Mod end x = X.new p x.super_method # => :super_method p x.mod_method # => :mod_method
Module#ancestors
ここまで読んで『継承と mixin が複数あってややこしい…』と思うかもしれない。
そこでまずは継承関係を『継承リスト』という形で視覚化してみてみよう。
継承リストは Module#ancestors
メソッドで取得する事が出来る。
Module#ancestors
はクラスオブジェクトをレシーバとして呼び出すことができ『クラス、モジュールのスーパークラスとインクルードしているモジュール を優先順位順に配列に格納して返す』というメソッドである。
see:https://docs.ruby-lang.org/ja/latest/method/Module/i/ancestors.html
説明を聞いてもよくわからないと思うので実際に試してみよう。
例えば、Integer
であれば
p Integer.ancestors # => [Integer, Numeric, Comparable, Object, Kernel, BasicObject]
となり、String
であれば
p String.ancestors # => [String, Comparable, Object, Kernel, BasicObject]
となる。
これは単純に
BasicObject ↑ Kernel ↑ Object ↑ Comparable ↑ Numeric ↑ Integer
というような『継承関係にある』と考えれば理解しやすいだろう(厳密に『継承』と言ってしまうと正しくはないが…。
また継承リストには『レシーバのクラス』も含まれている。
ちなみにユーザが定義したクラスは暗黙的に Object
を継承することになる。
class X end p X.ancestors # => [X, Object, Kernel, BasicObject]
更に継承を行った場合は Object
と定義したクラスの間に継承したクラスが挿入される。
class Super end class Sub < Super end p Sub.ancestors # => [Sub, Super, Object, Kernel, BasicObject]
このように Module#ancestors
を使うことでそのクラスの『継承リスト』を確認する事が出来る。
メソッドを呼び出す優先順位
ここで少し継承と mixin から離れて『メソッドの探査』について考えてみよう。
と、言っても実はそんなに難しくない。メソッドの探査は
『継承リストの先頭から走査して最初に定義されているクラスのメソッドを実行する』
というような形になる。
class Super def mami "mami" end def homu "homu" end end class Sub < Super # Super クラスのメソッドを上書きする def homu "homuhomu" end end # Super よりも Sub のほうが優先順位が高い p Sub.ancestors # => [Sub, Super, Object, Kernel, BasicObject] sub = Sub.new # Sub では定義されていないので次の Super のメソッドが呼び出される p sub.mami # => "mami" # Sub で定義されているので Sub のメソッドが呼び出される p sub.homu # => "homuhomu" # Sub でも Super でも定義されていないので、Kernel で定義されているメソッドを呼び出す p sub.class # => Sub
ここで重要なのが『継承リストがそのままメソッド探査の優先順位になる』という点である。
逆にいえば『メソッド探査は継承リストに依存してる』ともいえる。
このあたりは重要なので覚えておくとよい。
余談だが、Method#owner
でメソッドが定義されているクラス/モジュールを確認する事が出来る。
sub = Sub.new p sub.method(:mami).owner # => Super p sub.method(:homu).owner # => Sub p sub.method(:class).owner # => Kernel
mixin したクラスの #acestors
をみてみる
これまでの流れで、
- 継承リストは
Module#ancestors
で取得する事が出来る - 継承リストには自身も含まれている
- メソッド探査の優先順位は継承リストに依存する
- 継承を行うと継承したクラスが継承リストに挿入される
と、いうことがわかったと思う。
では、#include
でモジュールを mixin をするとどうなるか。
実際に試してみよう。
module Mod end class X include Mod end # Mod がいる!! p X.ancestors # => [X, Mod, Object, Kernel, BasicObject]
あれ、 mixin しても継承リストに追加されてる!?
そう、mixin も実は単に継承リストに追加されるだけなのだ。
ちなみに先程から継承リストに存在している Kernel
も実はクラスではなくて『Object
に mixin されているモジュール』なのである。
p Kernel.class # => Module
継承と mixin を両方使ってみると
では、継承と mixin の両方を行うとどうなるのか試してみよう。
class Super end module Mod end class X < Super include Mod end p X.ancestors # => [X, Mod, Super, Object, Kernel, BasicObject]
上のコードの実行結果を見てもらうと Mod
モジュールとと Super
クラスが継承リストに追加されていることがわかる。
また、継承リストから見てわかる通り、継承と mixin を行った場合は mixin のほうが前に挿入されていることがわかる。
これにより『mixin を行ったモジュールのメソッドを優先して』呼び出されることになる。
class Super def homu "Super#homu" end end module Mod def homu "Mod#homu" end end class X < Super include Mod end p X.new.homu # = > "Mod#homu"
ちなみに複数のモジュールを mixin した場合は『あとから mixin したモジュール』の優先順位が高くなる。
module Mod1 end module Mod2 end class X include Mod1 include Mod2 end p X.ancestors # = > [X, Mod2, Mod1, Object, Kernel, BasicObject]
このように継承も mixin も『継承リストに挿入されているだけ』に過ぎないのである。
prepend
する
ところで Ruby 2.0 で Module#prepend
というメソッドが追加された。
これは #include
と同様にモジュールを mixin する機能であるが、『優先順位が mixin されたクラスよりも高い』という特性がある。
module Mod def homu "Mod#homu > #{super()}" end end class X prepend Mod def homu "X#homu" end end # X#homu ではなくて Mod#homu が呼ばれる p X.new.homu # "Mod#homu > X#homu"
このように #prepend
したモジュールのメソッドが自身のクラスのメソッドよりも優先して呼ばれる。
では、これも継承リストを見てみよう。
module Include end module Prepend end class X include Include prepend Prepend end p X.ancestors # => [Prepend, X, Include, Object, Kernel, BasicObject]
#include
したモジュールと比較すると #prepend
したモジュールが『自身のクラスよりも前に』継承リストに挿入されている事がわかる。
このようにして継承リストを確認することで『自身よりも #prepend
したモジュールが優先して呼ばれる理由』も納得すると思う。
こうしてみると難しそうな #prepend
も単純に『継承リストに挿入する位置が違うだけ』なのがわかると思う。
まとめると
#include
はクラスよりも前に継承リストに挿入される#prepend
はクラスよりも後に継承リストに挿入される
という形になる。
singleton_class.ancestors
をみている
ここまで来たら特異メソッドについても考えみよう。
Ruby ではクラスだけではなくて『インスタンスに対しても』メソッドを定義することができる。
また、そのメソッドはクラスで定義されているメソッドよりも優先して呼ばれる。
class X def homu "X#homu" end end x = X.new def x.homu "x.homu" end p x.homu # => "x.homu"
このように『インスタンスに対して定義されるメソッド』のことを『特異メソッド』と呼ぶ。
また、特異メソッドは『特異クラス』という特殊なクラスに定義される。
この特異クラスは『各オブジェクトが必ず1つ持っている』クラスになる。
class X end # X クラスの特異クラス p X.singleton_class # => #<Class:X> x = X.new # x インスタンスの特異クラス p x.singleton_class # => #<Class:#<X:0x00000001275e10>>
ちょっと複雑になってきたので特異メソッドと特異クラスはこのあたりにしてメソッド探査の話をしよう。
メソッド探査が継承リストに依存しているのはこれまで話したとおりである。
と、いうことで特異クラスの継承リストを確認してみよう。
class X end x = X.new p x.singleton_class # => #<Class:#<X:0x00000001828790>> p x.singleton_class.ancestors # => [#<Class:#<X:0x00000001828790>>, X, Object, Kernel, BasicObject]
上のコードを見てもらうと『特異クラスが自身の継承リストに追加されている』事がわかる。
なのでメソッド探査に依存する継承リストというのは正確にいえば『自身の特異クラスの継承リスト』ということになる。
これにより『クラスで定義されたメソッド』よりも『特異クラスで定義されたメソッド=特異メソッド』が優先されるのである。
#extend
してみる
最後に #extend
について説明しておく。
これも #include
や #prepend
と同様にモジュールを mixin する機能であるが『特異クラスに mixin する』という点が他のメソッドと異なる。
module Mod def homu "Mod#homu" end end class X end x = X.new # Mod のメソッドが x オブジェクトに定義される x.extend Mod p x.homu # => "Mod#homu" # 特異クラスの継承リストに extend したモジュールが追加されている p x.singleton_class.ancestors # => [#<Class:#<X:0x000000017a51b0>>, Mod, X, Object, Kernel, BasicObject]
このように #extend
は『特異クラスの継承リスト』に追加される。
逆にいえば
x.extend Mod
は
x.singleton_class.include Mod
と同等ともいえるだろう。
おまけ:特異メソッドよりも優先するメソッドを定義する
おまけ。
特異クラスに対して #prepend
することで特異メソッドよりも優先されるメソッドを定義することが出来る。
module Mod def homu "Mod#homu" end end class X end x = X.new def x.homu "x.homu" end p x.homu # => "x.homu" p x.singleton_class.prepend Mod p x.homu # => "Mod#homu" # 特異クラスよりも前に挿入されている事がわかる p x.singleton_class.ancestors # => [Mod, #<Class:#<X:0x00000001094dd0>>, X, Object, Kernel, BasicObject]
滅多にないと思うが一番優先したいメソッドを定義したい場合はこういう手段が考えられる。
まとめ
- メソッド呼び出しの優先順位は継承リストに依存する
- 継承リストは
Module#ancestors
で確認できる - 継承も mixin も継承リストにクラス/モジュールを挿入する機能に過ぎない
#include
はクラスよりも前に継承リストに挿入される#prepend
はクラスよりも後に継承リストに挿入される- 特異メソッドは特異クラスに定義される
- 特異クラスは継承リストの一番手間に挿入されるのでメソッド探査の優先順位が一番高い
- メソッドの探査はレシーバの『特異クラスの』継承リストに依存する
#extend
は特異クラスの継承リストに挿入される
このように継承や mixin は継承リストが中心となって機能していることがみてとれる。
メソッド探査のまとめは以下のようになる。
スーパークラス ↑ include したモジュール ↑ 自身のクラス ↑ prepend したモジュール ↑ インスタンスが extend したモジュール ↑ インスタンスの特異クラス(特異メソッド)
Ruby では外部で定義されているメソッドを取り込む機能として継承や include
、prepend
など手段が分散されており複雑なので理解するのが難しい。
しかし、継承リスト(#ancestors
)を参照することでなんとなく仕組みがわかったのではないだろうか。
もし、意図しないメソッドが呼び出される問題が発生した場合は継承リストを確認してみるとよいだろう。
ちなみに特異クラスでも #include
や #prepend
を行うことができるので、継承リストがどうなるのか気になるのであれば手元で試してみるとよいだろう。