Ruby
メタプログラミング

特異メソッドを生やすときにスコープを引き継ぎたい

More than 3 years have passed since last update.

メタプログラミングが強力な Ruby では、特異メソッドというオブジェクト固有のメソッドをその場で定義することが出来ます。

hoge = "hoge"
def hoge.piyo
  "piyo"
end
hoge.piyo # => "piyo"

def hoge.piyo の内側はインスタンスのスコープのため、定義時のローカル変数やメソッドは参照できません。

hoge = "hoge"
message = "piyo"
def hoge.piyo
  message
end
hoge.piyo # => NameError: undefined local variable or method `message' for "fuga":String

なんとかしてローカル変数やメソッドを参照したいです。

ローカル変数を参照する

Object#define_singleton_method を使うことで、定義時のローカル変数を特異メソッドから参照することができます。

hoge = "hoge"
message = "piyo"
hoge.define_singleton_method(:piyo) do
  message
end
hoge.piyo # => "piyo"

Ruby のブロックはクロージャです。define_singleton_method に渡したブロック内では、定義時のスコープが引き継がれており、message を参照できます。

メソッドを参照する

メソッドを参照する場合、同じように define_singleton_method を使うだけではうまくいきません。

class Piyo
  def message
    "piyo"
  end

  def def_piyo(obj)
    obj.define_singleton_method(:piyo) do
      message
    end
  end
end

hoge = "hoge"
Piyo.new.def_piyo(hoge)
hoge.piyo # => NameError: undefined local variable or method `message' for "hoge":String

define_singleton_method に渡したブロックは、selfdefine_singleton_method のレシーバすなわち hoge の状態で実行されます
そのため、messagehoge.message と解釈され、エラーとなります。

解決方法はいろいろあります。

a. self を退避する

class Piyo
  def message
    "piyo"
  end

  def def_piyo(obj)
    self_orig = self
    obj.define_singleton_method(:piyo) do
      self_orig.message
    end
  end
end

hoge = "hoge"
Piyo.new.def_piyo(hoge)
hoge.piyo # => "piyo"

self を適当な変数に避けておけば、定義時の self を参照できます。

b. Method オブジェクトに固める

class Piyo
  def message
    "piyo"
  end

  def def_piyo(obj)
    message_method = method(:message)
    obj.define_singleton_method(:piyo) do
      message_method.call
    end
  end
end

hoge = "hoge"
Piyo.new.def_piyo(hoge)
hoge.piyo # => "piyo"

Method オブジェクトはレシーバが self に依存しないので、ふつうに呼べます。

c. 実行結果をローカル変数に入れておく

class Piyo
  def message
    "piyo"
  end

  def def_piyo(obj)
    message_value = message
    obj.define_singleton_method(:piyo) do
      message_value
    end
  end
end

hoge = "hoge"
Piyo.new.def_piyo(hoge)
hoge.piyo # => "piyo"

一番ラクな方法かもしれないが、message メソッドが def_piyo 実行時に呼ばれてしまうので、都合が悪い場合も多そう。

ブロックが実行されるときに、self が置き換えられる という考え方は直感的なので、これを理解していると ***_eval 系でもハマることが少なくなるかもしれません。

(おまけ) 特異クラス

Object#singleton_class でオブジェクトの特異クラスを取得できます。特異クラスは、オブジェクトの特異メソッドを保有するクラスです。

hoge = "hoge"
def hoge.piyo
  "piyo"
end
hoge.singleton_class # => #<Class:#<String:xxx>>
hoge.singleton_class.instance_methods - String.instance_methods # => [:piyo]

上例では、hoge.singleton_class がインスタンスメソッドとして piyo を持っていることが分かります。

オブジェクトの特異クラスで defdefine_method することで、特異メソッドを生やせます。

hoge = "hoge"
message = "piyo"
hoge.singleton_class.class_eval do
  define_method(:piyo) do
    message
  end
end
hoge.piyo # => "piyo"

ただし、def だとスコープが変わるのでローカル変数は使えません。

hoge = "hoge"
message = "piyo"
hoge.singleton_class.class_eval do
  def piyo
    message
  end
end
hoge.piyo # => NameError: undefined local variable or method `message' for "hoge":String

クラス名はただの定数なので、singleton_class を定数に代入すれば class 定義文でもオープンできます。class << object 構文と同じ意味になります。

hoge = "hoge"
HogeSingletonClass = hoge.singleton_class
class HogeSingletonClass
  def piyo
    "piyo"
  end
end
hoge.piyo # => "piyo"

参考