Rubyのモジュール関数を理解しよう!

ブログを下記に移転しました。デザイン変更により移転先では記事が一層読みやすくなっていますので、よろしければ移動をお願い致します。

Rubyのモジュール関数を理解しよう! : melborne.github.com

                                                                                                            • -

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内では以下のような処理が実行されます

  1. extendを使ってColorモジュールの抽象クラスのコンテキストで、redメソッドにアクセスできるようにする
  2. alias_methodにより、redメソッドをnew_methに別名定義する*1
  3. define_methodにより、インスタンス・メソッドと同じ内容のモジュール・メソッドredを定義する
  4. インスタンス・メソッド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)

*1:aliasではうまくいかない