第2回: モジュールによる mix-in [Ruby 2.5]

(2019.10) Ruby 2.5で確認。

「Rubyには多重継承はない」とよく言われる。しかし、そうではない。菱形継承もある。

モジュール

Ruby のモジュールは、メソッドを定義できてクラスと似ています。ただ、次の制限があります。

  • インスタンス化 (インスタンスの生成) ができない
  • 継承したりさせたりできない 継承できます (includeメソッドで)。

ほかのクラスにincludeしたり、オブジェクトにextendして、モジュールのメソッドを追加させることができます。

クラス/オブジェクトには複数のモジュールを付けられます。多重継承とほぼ同じ, メソッド定義の共有ができます。

クラスの関係を見てみると、モジュールはModuleクラスのインスタンスになります。Classクラスの直接のインスタンスではないので、モジュールをインスタンス化できないのは明らかです。

余談。

ClassクラスはModuleクラスのサブクラスになっています。ありがちな設計上の誤りで、本来継承関係にないものを継承させてしまっていると思います。

例えば、次の現象が起こっています;

  • Classクラスで、Moduleクラスで定義されたメソッドのいくつかを削除している
  • モジュールが書けるところにクラスが書けるとは限らない

Moduleクラスで定義されているメソッドについても、モジュールに関係ないものが多くあります。だいぶ混乱しています。

モジュールの定義, include

モジュールは次のようにして定義します。メソッドはインスタンスメソッドの形で定義します。クラスメソッドは後述。

Ruby
[RAW]
  1. module M
  2. p self #=> M
  3. def f
  4. "f: #{self}"
  5. end
  6. end
  7. class C
  8. include M
  9. end
  10. class D; end
  11. class E < C
  12. include M # 菱形継承
  13. end
  14. class F < D
  15. include M
  16. end
  17. p E.ancestors #=> [E, C, M, Object, Kernel, BasicObject]
  18. p F.ancestors #=> [F, M, D, Object, Kernel, BasicObject]
  19. i = C.new
  20. p i.f #=> "f: #<C:0x0000000000fed940>" selfがCインスタンス, に注意.

定義したモジュールは、クラスでincludeします。モジュールのメソッドがオブジェクトのメソッド呼び出し経路に挿入されます。ancestors クラスメソッドで確認できます。自クラス -> includeされたモジュール -> スーパークラス、の順です。

よくある解説ではクラス E にある include M は無視される、とあるが、正しくは菱形継承になっている。

スーパークラスよりも includeされたモジュールのほうが呼び出される優先順位が高いにも関わらず、スーパークラスで include するとサブクラスのそれが無視される、という組み合わせなので、スーパークラスでのデザイン変更が、サブクラスに影響してしまう。

先に宣言したほうが優先になっていないことによる混乱。

一つのクラス内で複数のモジュールを別のinclude文で includeした場合は、後からincludeされた方が、先に検索されます。

モジュールでインスタンス変数を使ってはならない

Ruby のインスタンス変数の領域は、クラスのインスタンスでただ一つだけが確保されます。includeされたモジュールのメソッドでインスタンス変数を使うと、異常な挙動になる危険があります。

次の例は, クラス、モジュールの各メソッドで, @hoge を使っています。

Ruby
[RAW]
  1. module M1
  2. def x
  3. @hoge = 100
  4. end
  5. def z
  6. return @hoge # 100を期待
  7. end
  8. end
  9. module M2
  10. def y
  11. @hoge = "str"
  12. end
  13. end
  14. class D
  15. # mix-in ということになっている
  16. include M1, M2 # この場合は, M1, M2の順序
  17. def f
  18. @hoge = "fuga"
  19. x()
  20. y()
  21. return z()
  22. end
  23. def g; return @hoge end # "fuga"を期待
  24. end
  25. p D.ancestors #=> [D, M1, M2, Object, Kernel, BasicObject]
  26. obj = D.new
  27. p obj.f #=> "str"
  28. p obj.g #=> "str"

メソッドM1#z() は期待に反して "str" になり, D#g()も同様です。

かなり不味い。エラーも出ないので、分かりにくいバグの温床です。

運用ルールとして, モジュールのインスタンスメソッドでは, (1) インスタンス変数を使うことを禁止するか, (2) インスタンス変数名にモジュール名を含めるなどしてほかのクラスやモジュールと名前が被らないようにしなければなりません。

多重継承における状態の持ち方の問題です。

Common Lisp は, 多重継承を認めるがメンバ変数が共有される。Ruby に似た挙動 (運用で気をつける)。Python3 では, 多重継承がありメンバ変数が共有されるのも同じだが、プライベート変数は自動的にクラス名が補われるので, 衝突しにくく実害が起きにくい。

Java8 からの interfaceのデフォルト実装は、ほぼ多重継承だが, 変数は自動的に public static final になり、状態を持てない。C# 8 (.NET Core 3.0 / .NET Standard 2.1) からのinterfaceのデフォルト実装も、Java8と同じ考え方。

C++は、多重継承ありでクラスごとにメンバ変数を持つので、上記の問題とは世界が違う。

モジュールでもincludeできる

クラスだけでなく、モジュール内で includeできます。多重継承です。菱形継承もできます。

Ruby
[RAW]
  1. module D end
  2. module B
  3. include D
  4. end
  5. module C
  6. include D
  7. end
  8. module E end
  9. class A
  10. include E
  11. include C
  12. include B
  13. end
  14. p A.ancestors #=> [A, B, C, D, E, Object, Kernel, BasicObject]

この例では、Common Lisp と同じになる。

(defclass D () ())
(defclass B (D) ())
(defclass C (D) ())
(defclass E () ())

; 左が優先
(defclass A (B C E) ())

(setq obj (make-instance 'A))

(print (sb-mop:class-precedence-list (find-class 'A)))
(format t "~%") ; ~% = newline

実行結果。Rubyと同じ順序です。

(#<STANDARD-CLASS COMMON-LISP-USER::A> #<STANDARD-CLASS COMMON-LISP-USER::B>
 #<STANDARD-CLASS COMMON-LISP-USER::C> #<STANDARD-CLASS COMMON-LISP-USER::D>
 #<STANDARD-CLASS COMMON-LISP-USER::E>
 #<STANDARD-CLASS COMMON-LISP:STANDARD-OBJECT>
 #<SB-PCL::SLOT-CLASS SB-PCL::SLOT-OBJECT> #<SB-PCL:SYSTEM-CLASS COMMON-LISP:T>)

オブジェクトにメソッドを付ける (extend)

モジュールのメソッドは、extendで、オブジェクトに付けることもできます。

Ruby
[RAW]
  1. module M
  2. def f; p "#{self} M#f()!!" end
  3. end
  4. class D
  5. extend M
  6. end
  7. p D.ancestors #=> [D, Object, Kernel, BasicObject] Mは表示されない
  8. D.f #=> "D M#f()!!"
  9. class E; end
  10. obj = E.new
  11. obj.extend M # このオブジェクトにのみメソッドを付ける.
  12. p E.ancestors #=> [E, Object, Kernel, BasicObject]
  13. p obj.class.ancestors #=> [E, Object, Kernel, BasicObject] 変化なし
  14. obj.f #=> "#<E:0x0000000001fe6450> M#f()!!"

すでに見てきたように、オブジェクトに直接付けるメソッドは、特異クラスのインスタンスメソッドですから、extendは、次と同じ意味です。

Ruby
[RAW]
  1. class D
  2. # extendは次と同じ
  3. class << self
  4. include M
  5. end
  6. end

include, extendはモジュールしか引数に取れません。すでに述べたように、ClassModuleのサブクラスになっているのがおかしいのです。

モジュールのクラスメソッド

モジュールのクラスメソッドは、定義したモジュールをレシーバにしてしか呼び出せません。

これを逆手に取って、関数の名前空間を明確にするのに使えます。

Ruby
[RAW]
  1. module M
  2. @v = 100
  3. def self.g
  4. p self
  5. p @v
  6. end
  7. # 特異クラスのインスタンスメソッドがクラスメソッド
  8. class << self
  9. def h; p self end
  10. end
  11. end
  12. class C
  13. include M
  14. def f
  15. M.g # 単にg()ではエラー
  16. end
  17. end
  18. M.g #=> M, 100
  19. M.h #=> M
  20. C.new.f #=> M, 100

レシーバを明記しないと呼び出せないので、インスタンス変数は、常にインスタンスとしての M のものが使われます。