tnakata's blog

Webエンジニア(2017/01/01~)。プログラミングやWebについて語る。学生時代は航空宇宙工学を専攻してた。

Rubyではなぜmix-inでクラスメソッドを引き継げないのか

includeと継承の違い

includeメソッドによりモジュールをinclude(Mix-in)してそのモジュールのインスタンスメソッドを使えるようになります。クラスメソッドは使えません。

instance method Module#include (Ruby 2.4.0)

一方、継承では親となるクラスのインスタンスメソッド・クラスメソッドの両方を使えるようになります。

module Foo
  def foo_instance_method
    "foo_instance_method"
  end
  def self.foo_class_method
    "foo_class_method"
  end
end

class SuperBoo
  def boo_instance_method
    "boo_instance_method"
  end
  def self.boo_class_method
    "boo_class_method"
  end
end

class Boo < SuperBoo
  include Foo
end

boo = Boo.new
boo.boo_instance_method #=> "boo_instance_method"
boo.foo_instance_method #=> "foo_instance_method"

Boo.boo_class_method #=> "boo_class_method"
Boo.foo_class_method #=> NoMethodError: undefined method `foo_class_method' for Boo:Class

上記でBooクラスがFooモジュールをincludeしたことでBooクラスの継承チェーンにFooモジュールが組み込まれます。

Boo.ancestors #=> [Boo, Foo, SuperBoo, Object, PP::ObjectMixin, Kernel, BasicObject]

ここだけ見ると、includeは継承の一種かなと思ってしまいます。違いが出るのは特異クラスの継承チェーンを見たときです。

Boo.singleton_class.ancestors #=> [#<Class:Boo>, #<Class:SuperBoo>, #<Class:Object>, #<Class:BasicObject>, Class, Module, Object, PP::ObjectMixin, Kernel, BasicObject]

上記で確認できるように、includeしたモジュールは特異クラスの継承チェーンには出てきません。このため、includeしたモジュールのクラスメソッドを使用できません。つまり、includeと継承は一見似ているようですが全く異なる処理となります。

includeしたモジュールのクラスメソッドも使いたい

上記の通り、includeしたモジュールからはクラスメソッドを引き継げません。このため、ある責務を持ったモジュールのクラスメソッドを複数のクラスへ引き継ぎたいという場合にどうするかという問題が出てきます。この場合はincludedを使った実装をするのが一般的です。

qiita.com

module Foo
  def foo_instance_method
    "foo_instance_method"
  end
  def self.included(base)
    base.extend(ClassMethods)
  end
  module ClassMethods
    def foo_class_method
      "foo_class_method"
    end
  end
end

class SuperBoo
  def boo_instance_method
    "boo_instance_method"
  end
  def self.boo_class_method
    "boo_class_method"
  end
end

class Boo < SuperBoo
  include Foo
end

Boo.foo_class_method #=> "foo_class_method"

上記の実装はRailsでも使用されています。

なぜincludeでクラスメソッドを継承しないのか

そもそもなぜincludedを使用するようなめんどくさいことをしないとincludeしたモジュールのクラスメソッドを引き継げないのでしょうか。

上記で述べたようにincludeでクラスメソッドを引き継ぐ、という要望は普通にあるみたいです。であれば、Rubyの仕様としてincludeしたモジュールのクラスメソッドも引き継ぐというのはありだったんじゃないか、ということを考えました。その方がincludedを使用せずともクラスメソッドが使えるようになるから楽ですよね。

多重継承と同じになってしまうから、というのも考えましたがまあこれは違うかと。複数のモジュールをincludeしても継承しても、継承チェーンの中での順番は確定するからです。

とはいえ、includeでクラスメソッドを継承しないという選択をしているからには何かしらのメリットがあるということです。このメリットが何か私にはわからなかったので調べてみることにしました。

includeが使用されるそもそもの想定とは

Ruby forumの過去のスレッドで同じようなことを疑問に思った人がスレッドを立てていました。

Why the lack of mixing-in support for Class methods? - Ruby Forum

疑問に対するmatzさんの回答もありました。

https://www.ruby-forum.com/topic/68638

引用します。

Mix-in is used for several purposes and some of them can be hindered by inheriting class methods, for example, I don’t want to spill internal methods when including the Math module. I am not against for some kind of inclusion that inherits class methods as well, but it should be separated from the current #include behavior.

以下、matzさんの考えへの私の解釈です。

Mix-inが使用される想定(例えば、後述するMathモジュールみたいな使い方)がいくつかあるんだけど、クラスメソッドを継承したらそれがうまくいかない。 例えば、Mathモジュールを継承するときにクラスメソッドまで継承したら、includeしたクラスでメソッド名(名前空間)がかなり侵されてしまうのを避けたい。Mathモジュールの使い方を見ると、インスタンスメソッドとクラスメソッドにおそらく同名のメソッドを持っています。MathモジュールをincludeすればMathモジュールのクラスメソッドは不要になりますが、この時クラスメソッドも継承していると不要になったクラスメソッドの分だけincludeしたクラスでメソッド名の衝突が起きてしまう恐れがあります。