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ではうまくいかない


blog comments powered by Disqus
ruby_pack8

100円〜で好評発売中!
M'ELBORNE BOOKS