RubyのModuleを復習した
Rubyのモジュールについて、includeとかextendとか諸々の理解が怪しいなと思ったので復習した。
業務ではActiveSupport::Concernの恩恵を受けていることで、純粋にモジュールを扱ったことがなかった。というかモジュールを理解していないとConcernを理解しているとも言えないと思う。つまるところなにもわかってない。
復習に使ったバージョンは以下のとおり。
❯ ruby -v
ruby 3.0.3p157 (2021-11-24 revision 3fb7d2cadc) [x86_64-darwin21]
参考資料
プロを目指す人のためのRuby入門 伊藤淳一[著]
通称『チェリー本』。非常にわかりやすく書かれているけど浅い理解で終わることを許してくれない良書。最近第2版が出た。
APIドキュメント
モジュールはクラスではない
チェリー本に書かれているモジュールの特徴は以下の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という委譲用のモジュールがあるらしい)など本当に色々な実現方法がある。数ある選択肢の中から適切なものを選ぶにはまだまだ理解できていない事が多い。
チョットデキルまでの道のりは長い。