TIL

RubyのModuleを復習した

Rubyのモジュールについて、includeとかextendとか諸々の理解が怪しいなと思ったので復習した。

業務ではActiveSupport::Concernの恩恵を受けていることで、純粋にモジュールを扱ったことがなかった。というかモジュールを理解していないとConcernを理解しているとも言えないと思う。つまるところなにもわかってない。

復習に使ったバージョンは以下のとおり。

❯ ruby -v
ruby 3.0.3p157 (2021-11-24 revision 3fb7d2cadc) [x86_64-darwin21]

参考資料

プロを目指す人のためのRuby入門 伊藤淳一[著]
通称『チェリー本』。非常にわかりやすく書かれているけど浅い理解で終わることを許してくれない良書。最近第2版が出た。

https://gihyo.jp/book/2017/978-4-7741-9397-7

APIドキュメント
https://rubyapi.org/3.0/o/module

モジュールはクラスではない

チェリー本に書かれているモジュールの特徴は以下の2つ。

  • モジュールはインスタンス化できない
  • モジュールは他のモジュールやクラスを継承できない

クラスじゃないのでインスタンス化や継承はできない、納得。

と言ってもモジュールはModuleというクラスで実現されているので、まあまあ混乱する。

module HelloWorldable
  def hello
  end
end

p HelloWorldable.class  #=> Module

モジュールの主な用途

チェリー本によるとモジュールの主な用途は以下のとおり。

  • 継承を使わずにクラスにインスタンスメソッドを追加もしくは上書き
  • 複数のクラスに共通のクラスメソッドを追加
  • クラス名や定数名の衝突を防ぐために名前空間を分ける
  • 関数的メソッドを定義する
  • シングルトンオブジェクトのように扱って定数値などを保持

パッと見るとインスタンスメソッドの追加と、名前空間を分ける用途が一番お世話になってそう。

モジュールによるMix-in

モジュールを使ったメソッドの追加(ミックスイン)にはいくつか方法があり、どれ使えばいいの状態になるので整理する。

include

一番基本的なミックスインがincludeを使う方法。クラスにインスタンスメソッドを追加できる。

module HelloWorldable
  def hello
    puts "hello, world!"
  end
end

class Human
  include HelloWorldable
end

person = Human.new

person.hello #=> hello, world!

クラスに同名のインスタンスメソッドが存在した場合はクラス側が優先される。

module HelloWorldable
  def hello
    puts "hello, world!"
  end
end

class Human
  include HelloWorldable

  def hello
    puts "hello!"
  end
end

person = Human.new

person.hello #=> hello!

extend

モジュールのメソッドをインスタンスメソッドではなく、クラスメソッド(特異メソッド)として追加するのがextend。extendは継承の意味で使われることが多い単語なので気をつけたい。

module HelloWorldable
  def hello
    puts "hello, world!"
  end
end

class Human
  extend HelloWorldable

  def hello
    puts "hello!"
  end
end

Human.hello  #=> hello, world!

person = Human.new
person.hello #=> hello!

クラスメソッドとしての追加なので、元々クラスが持つインスタンスメソッドも使える。

おまけとして、Objectに生えてるextendメソッドにモジュールを渡すことで、インスタンスに対しての特異メソッドとしても追加できるらしいが、利用ケースは少なそう。

prepend

includeの上書き版。インスタンスメソッドを追加するのはincludeと同じだが、ミックスイン先に同名のメソッドがあった場合にモジュール側が先に呼ばれる。

module HelloWorldable
  def hello
    puts "hello, world!"
  end
end

class Human
  prepend HelloWorldable

  def hello
    puts "hello!"
  end
end

person = Human.new
person.hello #=> hello, world!

このコード例だと全然旨味がないけど、オープンクラスで既存のメソッドの挙動を変えたい時などには使えそう。まあWebアプリケーションを素直に作ってる時に多用する機能ではないと思う。

上書き版と書いてしまったが、モジュール側でsuperを使うことでミックスイン先の同名メソッドを呼び出せる。

module HelloWorldable
  def hello
    puts "hello, world!"
    super
  end
end

class Human
  prepend HelloWorldable

  def hello
    puts "hello!"
  end
end

person = Human.new
person.hello #=> hello, world!
             #=> hello!

ミックスイン先のメソッドが使える

モジュール側からミックスイン先のメソッドを呼び出せる。これは結構難しい。

メソッドを呼び出せるだけでなく、モジュールのメソッドに暗黙的に渡されているselfはミックスイン先のインスタンス(extendを使った場合はクラス)なので、インスタンス変数にアクセスすることも可能。もちろん無闇にしないほうがいい。

module HelloWorldable
  def hello
    self.goodbye
  end
end

class Human
  include HelloWorldable

  def goodbye
    puts "Bye!"
  end
end

person = Human.new
person.hello #=> Bye!

メソッド探索順

モジュールやら継承やらが込み入ってきて同名メソッドがあふれると「結局どれが優先されるの?」的な状態になるが、そういう時はancestorsを使えば良いらしい。

module M1
  def hello
    puts "こんにちは!!"
  end
end

module M2
  def hello
    puts "やあ!!"
  end
end

module M3
  def hello
    puts "わん!!"
  end
end

class Parent
  include M3
  def hello
    puts "おなかすいた!!"
  end
end

class Child < Parent
  include M1
  include M2
  def hello
    puts "Hello!!"
  end
end

obj = Child.new
obj.hello #=> Hello!!

p Child.ancestors #=> [Child, M2, M1, Parent, M3, Object, Kernel, BasicObject]

基本的には子側が優先される考えで、困ったらancestorsを使えばいいと覚えておく。includeの記述順でメソッド探索順が変わるのはハマりポイントになるかもしれない。

ついでに、このメソッドがどこで定義されたものかを確認するのは、Methodクラスのownerで確認できる。

module HelloWorldable
  def hello
    puts "hello, world!"
  end
end

class Human
  include HelloWorldable
end

person = Human.new

p person.method(:hello).owner #=> HelloWorldable

モジュールの特異メソッド

こんなものまであるのか、という機能。module_functionを使うことで、ミックスインせずに使える特異メソッドを定義できる。もちろん特異メソッドとしてだけでなく、ミックスインしても使える(混乱)。Mathモジュールなんかはこれで特異メソッドを定義してるそうな。

module HelloWorldable
  def hello
    puts "hello, world!"
  end
  
  module_function :hello
end

HelloWorldable.hello #=> hello, world!

デフォルトでpublicになる

これはうっかり忘れそう。

モジュールのメソッドはクラスと同じく何もしなければミックスイン先でpublicになる。必要がなければモジュール側でprivate指定にしておく。

最後に寄り道

モジュールを完全に理解したところで、ちょっと寄り道をしてみる。

実務でよくお世話になるpluckメソッド、配列に生えてるものはEnumerableモジュールのもので、ActiveRecord_Relationに生えてるのはActiveRecord::Calculationsというモジュールに定義されているものらしい。

irb(main):009:0> [].method(:pluck).owner
=> Enumerable
irb(main):010:0> Student.all.method(:pluck).owner
=> ActiveRecord::Calculations
irb(main):011:0> ActiveRecord::Calculations.class
=> Module
irb(main):012:0> Enumerable.class
=> Module

ActiveRecord_Relationのコードを見てみると、確かにCalculationsをincludeしている。その1行上でEnumerableもincludeしているので、pluckが同名メソッドとして複数存在しているものの、include順でCalculationsモジュールの方が優先されているようだ。(試しにancestorsを呼んでみたらとんでもない量の配列が返ってきた)

感想

わかってたことだけど、モジュールひとつ取ってみても奥が深い。クラスにメソッドを追加するにも色々な手段が提供されているのはRubyの柔軟さの現れなのかなと思う一方、自分が使うとなると悩むことが多そうだとも思った。

クラスになにかをさせたいと思った時、モジュールだけでなくRubyの継承や委譲(Forwardableという委譲用のモジュールがあるらしい)など本当に色々な実現方法がある。数ある選択肢の中から適切なものを選ぶにはまだまだ理解できていない事が多い。

チョットデキルまでの道のりは長い。


<< 記事一覧へ