rubyのモジュールの使い方
すこし身内のチャットで話題になって頭の整理をしたのでついでにここにまとめておく。慣れないとひっかかりそうなとこかも。
問題
ことの発端は、モジュールの特異メソッドを他のモジュールでも使うにはどうすればよいか?ということ。
TestModule2からTestModuleのメソッドを使いたかったので以下のようなコードを書いた。
module TestModule module_function def test1 puts "test" end end module TestModule2 include TestModule module_function def test2 puts "test2 start" test1 puts "test2 end" end end TestModule2.test2
ところが結果は、undefined local variable or method `test1' なんですかこれは!という問題。
概念の整理
ここで概念の整理をしておこう。「モジュールのインスタンスメソッド」は正式には違う名前かもしれないが、ここでは便宜的にそう呼ぶことにする。
- モジュールのインスタンスメソッド
- モジュール内でdef メソッド名 として定義されるメソッド。他のクラス定義やモジュール定義の中で、include モジュール名 とすることによって、モジュールのインスタンスメソッドがクラスやモジュールにmixinされる。最も一般的だと思われる使い方。
- モジュールの特異メソッド
- モジュール内でdef self.メソッド名 もしくは def モジュール名.メソッド名と定義されるメソッド。こうするとコードのどこからでもモジュール名.メソッドという形で呼びだすことができる。関係ある関数群をグルーピングするときによく用いられる。
- モジュールメソッド
- モジュールに同名の特異メソッドとインスタンスメソッドが定義されているものをこう呼ぶ。一例としてMathモジュール。Mathモジュールにあるメソッドはモジュールメソッドになっているので、Math::sqrtの形でも使えるし、なにかのクラス/モジュールにmixinすれば、その中からはsqrtという形で呼べる。
ちなみに、module_functionというのはそれ以後の定義したメソッドをモジュールメソッドにするもの。従って、TestModule中のtest1は特異メソッドでありかつインスタンスメソッドになる。TestModule2中のtest2も同様。ただし、インスタンスメソッドはプライベートになる。ここ注意。
includeとextend
rubyにはモジュールの関数をとりこむ命令includeが用意されている。これが本当は何をするのかちゃんと書くと、
- include
- クラス定義/モジュール定義の中にinclude モジュール名 と書くと、モジュールのインスタンスメソッドがクラス/モジュールのインスタンスメソッドとして定義される。例えば、モジュールAにインスタンスメソッドhogeが定義されていた場合、クラスBの定義中にinclude Aと書くとAのインスタンスメソッドhogeがそのままクラスBのインスタンスメソッドhogeとして定義される。
となる。ちなみに、Objectにはextendというメソッドが定義されていて、これもmixinの働きをもつ*1。
- extend
- obj.extend モジュール名 とすると、モジュールのインスタンスメソッドがオブジェクトobjの特異メソッドとして定義される。
moduleのメソッドを他のオブジェクトにmixinするにはこの二つの方法しかない。両方とも、モジュールのインスタンスメソッドを他のオブジェクトにmixinするものである。
そう、mixinできるのは、モジュールのインスタンスメソッドだけなのである。従ってモジュールの特異メソッドはmixinできない。mixinしたければインスタンスメソッドとして書け、ということ。
上のコードで起きてたこと
上のコードでは、TestModule中のtest1メソッドがmixinされてTestModule2のインスタンスメソッドになる。TestModule2中ではtest2がmodule_functionの効果でモジュールメソッドとなり、特異メソッドとしても定義される。これによって、TestModule2.test2の呼びだしには成功する。ところが、test1は特異メソッドとしてはmixinされていないので、特異メソッドtest2の中からは見えない。結果、test2からtest1を呼びだそうとしたところで、undefined local variable or method `test1'という託宣が下る。
このコードのままで、他のクラス定義にTestModule2をincludeした上で、そのインスタンスからtest2を呼べばエラーはでない。
じゃあ、っていうんでtest1メソッドがTestModule2の特異メソッドになるようにinclude TestModuleの代わりにextend TestModuleしたらどうなるか?上のコード自体はとおる。しかし、今度は他のクラス定義にTestModule2をincludeした上で、そのインスタンスからtest2を呼ぶとエラーがでる。これは、test1が特異メソッドなのでincludeされないからだ。
さらにこの場合にはおまけがついていて、TestModule2.test1というコードを実行するとエラーになる。今度はメソッドが見つからないわけじゃなくて、プライベートだから呼べません、といわれてしまう。この原因は、module_functionにあって、そもそもTestModuleでのtest1がプライベートメソッドになっているからだ。従って、TestModule2にもプライベートな特異メソッドとして定義されてしまう。ふー、これにはまいりました。
正解は?
結局、TestModule、TestModule2ともに特異メソッドとしても、mixin用としても使いたい場合はこうする。
module TestModule def test1 puts "test" end extend self #module_functionの代わり。mixinしてもプライベートにならないように end module TestModule2 include TestModule # TestModuleのインスタンスメソッドを extend TestModule # -(1) TestModule2の特異メソッド、インスタンスメソッド双方にmixin #module_function # -(2) def test2 puts "test2 start" test1 puts "test2 end" end #extend self # - (3) end TestModule2.test2
注意 test2を特異メソッドにしたいなら、(2)か(3)の行を追加する必要がある。(3)を選択するなら、(1)の行も不要。
ちゃんとリファレンスにあたってないので間違えてるかもしれません。
つっこみ、コメント歓迎。
でもちょっと考えてみたら、TestModule2の中でtest1を呼ぶときに、素直にTestModule::test1って呼んでおけばいい気がしてきた。ModuleにModuleをincludeするのって何か違和感があるので、includeもextendもなしで。
*1:mixinって言っちゃっていいのかな