前回の続き。Rubyで自作の外部モジュールを読み込む方法 - include と extend と module_function - (゚∀゚)o彡 sasata299's blogにあったうちrequireとincludeについて書いたので、次はmodule_functionとextendの話を。

module_functionは何のためにあるのか

まず前回のおさらい。

  • モジュールの中で特異メソッド(def self.hogeという形)として定義されたものはincludeするとクラスメソッドになるし、MyModule.hogeという形で呼べる
  • モジュールの中でメソッド(def fugaという形)として定義されたものはincludeするとインスタンスメソッドになる。が、MyModule.fugaという形では呼べない

で、module_function。これを使うとどうなるかというと、

module_function でインスタンスメソッドのように定義したメソッドを指定してあげると、そのメソッドは include しない場合は Zozom という名前空間で呼べて、include した後は include した package にメソッドが生えて、直接呼べるようになるようです。便利ですね〜。
Rubyで自作の外部モジュールを読み込む方法 - include と extend と module_function - (゚∀゚)o彡 sasata299's blog

ということになる。そう、便利。なんで便利なのか。

  • (例えばMathみたいな)ユーティリティ的なモジュールは、いちいちモジュール名付けて呼び出すの億劫。includeして組み込み関数っぽく使いたい
  • でもトップレベルでincludeしちゃうと名前空間汚染しちゃうし、モジュールのほんの一部のメソッドしか使わないのに必ずincludeするのも変だし、何をしてるのか明示的にしたいときもあるし、そういうときははMath.cos(x)みたいにして使いたい
  • でもいちいち同じ定義のメソッドをとプライベートメソッド両方作るとかアホくさい

とまあそういうわけで、両方同時に作ってくれるというModuleクラスの便利メソッドです。それだけだった。例に上げたMathモジュールはほとんどのメソッドがmodule_functionされてるので、Math.cosでもinclude Mathしてからcosでも使えて便利。

extendは特定のオブジェクトだけに効力を発揮する

includeはModuleとmainオブジェクトのメソッドだったけど、extendはObjectのメソッド。加えて、Moduleのincludeはクラスメソッド(特異メソッド)だけど、Objectのextendはインスタンスメソッド。これは何をするものかというと、「特定のオブジェクトにだけ、クラスのincludeを呼び出したような効果を与える」もの。ちょっとわかりづらいけど、

include を extend にしてもまったく同じ結果(Zozomを付けなくても呼べる)でした。
Rubyで自作の外部モジュールを読み込む方法 - include と extend と module_function - (゚∀゚)o彡 sasata299's blog

というのは微妙に間違い。というのも、トップレベルではまったく同じ結果なんだけど、以下のコードでは違う結果になる。

module Foo
  def hoge; "hoge!"; end
end 
  
module Bar
  def fuga; "fuga!"; end
end

class Baz 
end

include Foo
extend Bar

puts hoge # => "hoge!"
puts fuga # => "fuga!" 
    
baz = Baz.new
puts baz.hoge2 # => "hoge!"
puts baz.fuga2 # => NoMethodError

includeはクラスにモジュールの特徴を追加する、この場合Objectクラスを書き換えるので、Bazの中でもFooのメソッドはもちろん使える。でも、extendはオブジェクトの特異クラス、この場合は「"mainオブジェクトにとっての"Objectクラス」しか書き換えないので、大本のObjectクラスから派生したBazには影響がない。そして、こんなこともできる。

module Foo
  def hoge; "hoge!"; end
end 
  
module Bar
  def fuga; "fuga!"; end
end

class Baz
  include Foo
end

baz1 = Baz.new
baz2 = Baz.new
baz1.extend Bar
puts baz1.hoge2 # => "hoge!"
puts baz1.fuga2 # => "fuga!"
puts baz2.hoge2 # => "hoge!"
puts baz2.fuga2 # => NoMethodError

FooモジュールはBazクラスでincludeしてるのでbaz1でもbaz2でもhogeメソッドが使えるし、そのサブクラスにも引き継がれる。でもBarモジュールはbaz1でextendしたものなので、fugaメソッドが使えるようになるのはbaz1だけで、同じBazクラスのインスタンスのbaz2でも、もちろんサブクラスのインスタンスでも、fugaメソッドは未定義のままになる。というのがincludeとextendの違いでした。

おまけ: ModuleとClassの違い

まつもとゆきひろ コードの世界?スーパー・プログラマになる14の思考法まつもとゆきひろ コードの世界 スーパー・プログラマになる14の思考法
著者:まつもとゆきひろ
販売元:日経BP出版センター
発売日:2009-05-21
クチコミを見る

この本の最初の方にMix-inの概念について詳しく説明してるところがあるんだけど、それによるとモジュールってのはMix-inを言語としてサポートするために導入されたものらしい。モジュールとクラスの違いは実装の継承の仕方の違いに関わってくる。

  • モジュールは普通のクラスのサブクラスにはなれないが、include/extendの引数になって、クラスやモジュールに取り込める。また、複数取り込むことができる
  • クラスは他のクラスのサブクラスになることができるが、include/extendを使って他のクラスやモジュールに取り込むことはできない。また、継承元のクラスは一つに限られる

ということで、モジュールは「継承関係に縛られない、ある"役割"に関わる仕様や実装を定義する」のに使ったりする。Javaで言うところのインターフェースに近いけど、これは仕様だけをまとめるもの。Rubyのモジュールは仕様と実装をセットでまとめるもの。あとあれだ、関係ある変数・定数・ユーティリティーメソッドを一括りにする名前空間的な使い方もするね。というかMix-inに慣れないうちはそっちの方が良く見掛けると思う。

疲れた…

書いてる最中に「あれっ、これってこんなんなってたんだ」みたいなことに気付きまくって、どんどん記事書き直して調べなおす羽目になった。ひぃ。知らないで使ってるうちは大して違和感なかったんだけど、真面目に考え始めた直後はえらいややこしくて混乱した。大雑把にわかってきた後はなんてことないんだけどなー。