Rubyのモジュール関数を理解しよう!
RubyのMathモジュールには数学関数が定義されていて、それらは以下のようにモジュール・メソッドとして呼び出す使い方と、クラスにモジュールをインクルードして関数的に呼び出す使い方の、2種類の使い方ができるようになっています。
Math.sqrt 4 # => 2.0
Math.atan2(1, 1) # => 0.785398163397448
include Math
sqrt 4 # => 2.0
atan2(1, 1) # => 0.785398163397448
一方でこれらのメソッドはインクルードした場合、オブジェクトを指定するメソッド形式での呼び出しができないようにされています。
Object.new.sqrt 4 # => private method `sqrt' called for #<Object:0x1a414> (NoMethodError)
このような形式で定義されたメソッドを、Rubyでは「モジュール関数」と呼んでいます。モジュール関数はその利用の態様に応じて使い方を選べるので、その利便性を高めます。
早々自分でもモジュール関数redを備えたColorモジュールを定義してみます。
module Color
def self.red
:red
end
private
def red
:red
end
end
Color.red # => :red
include Color
red # => :red
Object.new.red # => private method `red' called for #<Object:0x2272c> (NoMethodError)
Colorモジュールにredインスタンス・メソッドとredモジュール・メソッドを定義し、インスタンス・メソッドの可視性をprivateにします。
モジュール・メソッドはSingletonクラスを使って定義してもいいですね。
module Color
class << self
def red
:red
end
end
private
def red
:red
end
end
Color.red # => :red
include Color
red # => :red
Object.new.red # => private method `red' called for #<Object:0x226a0> (NoMethodError)
これで完了!
と言いたいところですが、明らかにこれらのコードには問題があります。
そう、DRY原則に反しているのです。同じコードの繰り返しはその保守性を下げるのでいけません。改善しましょう。singletonクラスにColorモジュールをインクルードすることによって、コードの重複を回避します。
module Color
class << self
include Color
end
private
def red
:red
end
end
Color.red # => private method `red' called for Color:Module (NoMethodError)
include Color
red # => :red
Object.new.red # => private method `red' called for #<Object:0x230dc> (NoMethodError)
残念ながらredの可視性がprivateにされているので、Colorオブジェクトからredを呼び出せないようです。Singletonクラスへのインクルードはextendと等価ですから、extendも試してみます。
module Color
extend self
private
def red
:red
end
end
Color.red # => private method `red' called for Color:Module (NoMethodError)
include Color
red # => :red
Object.new.red # => private method `red' called for #<Object:0x23190> (NoMethodError)
やはりダメです。さて…
苦肉の策を考えました。
module Color
class << self
include Color
def Red
red
end
end
private
def red
:red
end
end
Color.Red # => :red
include Color
red # => :red
Object.new.red # => private method `red' called for #<Object:0x225ec> (NoMethodError)
呼び出しの問題は解決しましたが、2つのメソッド名が異なるという致命的な問題が発生しました。
あるいはsendを使って…
module Color
extend self
private
def red
:red
end
end
Color.send :red # => :red
include Color
red # => :red
Object.new.red # => private method `red' called for #<Object:0x227b8> (NoMethodError)
これではとてもモジュール関数とは呼べません。さてどうしたものでしょうか…
こうなったら最後の手段です。そう、メタプログラミングです!
Colorモジュールにmod_funcというモジュール・メソッドを定義して、その引数としてインスタンス・メソッドを渡すと、それを自動でモジュール関数にしてくれるよう実装してみます。
module Color
def self.mod_func(meth)
extend self
private meth
end
def red
:red
end
mod_func :red
end
Color.red # => private method `red' called for Color:Module (NoMethodError)
include Color
red # => :red
Object.new.red # => private method `red' called for #<Object:0x22984> (NoMethodError)
最初の試みは失敗に終わりました。mod_func内のprivateでインスタンス・メソッドredだけでなく、モジュール・メソッドredもプライベート化されてしまうようです。
今度はdefine_methodを使ってモジュール・メソッドredを別に定義してみます。
module Color
def self.mod_func(meth)
extend self
(class << self; self end).module_eval do
alias_method :new_meth, meth
define_method(meth) do |*args, &block|
new_meth(*args, &block)
end
end
private meth
end
def red
:red
end
mod_func :red
end
Color.red # => :red
include Color
red # => :red
Object.new.red # => private method `red' called for #<Object:0x21bec> (NoMethodError)
今度はうまくいきました!mod_func内では以下のような処理が実行されます。
- extendを使ってColorモジュールの抽象クラスのコンテキストで、redメソッドにアクセスできるようにする
- alias_methodにより、redメソッドをnew_methに別名定義する1
- define_methodにより、インスタンス・メソッドと同じ内容のモジュール・メソッドredを定義する
- インスタンス・メソッドredをプライベートにする
mod_funcはモジュールにおいて汎用的に使えるので、これをColorモジュールだけの機能としておくのはもったいないです。Moduleクラスに移しましょう。
class Module
def mod_func(meth)
extend self
(class<<self;self end).module_eval do
alias_method :new_meth, meth
define_method(meth) do |*args, &block|
new_meth(*args, &block)
end
end
private meth
end
end
module Color
def red
:red
end
mod_func :red
end
Color.red # => :red
include Color
red # => :red
Object.new.red # => private method `red' called for #<Object:0x22150> (NoMethodError)
すてきです。
ええ、もちろんRubyはユーザにこんな手間を強いることはありません。Rubyにはモジュール関数を作るために、Module#module_functionというメソッドが用意されています。
module Color
def red
:red
end
module_function :red
end
Color.red # => :red
include Color
red # => :red
Object.new.red # => private method `red' called for #<Object:0x22240> (NoMethodError)
module_functionが引数を取らない場合、それ以降に定義されたメソッドがモジュール関数の対象になります。
module Color
module_function
def red
:red
end
end
Color.red # => :red
include Color
red # => :red
Object.new.red # => private method `red' called for #<Object:0x22830> (NoMethodError)
- aliasではうまくいかない ↩
blog comments powered by Disqus