メタプログラミングが強力な 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
に渡したブロックは、self
が define_singleton_method
のレシーバすなわち hoge
の状態で実行されます。
そのため、message
は hoge.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 を持っていることが分かります。
オブジェクトの特異クラスで def
や define_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"