Ruby 1.9のmodule_evalとブロック付きメソッド呼び出し
WikiFormeをRuby 1.9でも動かせるようにしようとして、困った。
具体的に言うと、Ruby 1.9(ruby 1.9.0 (2008-01-06 revision 0) [i686-darwin9.1.0])だと↓このコードが動いてしまう。
class Test end def modify_test(&block) Test.module_eval(&block) end # ↓一度modify_testメソッドを通過してTest.module_evalを呼び出す modify_test { def func p "worked" end # ↓意図しない動作orz func #=> "worked" } # ↓意図しない動作orz func #=> "worked" # ↓これは意図通り Test.new.func #=> "worked"
一方、↓このコードは意図通りに動く。
class Test end # ↓Test.module_evalを直接呼び出す Test.module_eval { def func p "worked" end # func #=> NameErrorになる } # func #=> "worked" #=> NameErrorになる # ↓これは意図通り Test.new.func #=> "worked"
前者だと、def 〜 endで定義したfuncメソッドが、Testクラスに定義されずにObjectクラスに定義されてしまっている。つまり、Ruby 1.9ではブロック付きメソッド呼び出しで受け取ったブロックをmodule_evalに渡すのと、最初からmodule_evalにブロックを渡すのとで動作が異なるらしい。(あるいはバグ?)
この例だと直接module_evalを呼べば解決する話なのだけど、私は↓こんな言語内DSL風なプログラムを書きたいんです。
class Element class << self # process { ... } でprocessメソッドを定義する def process(&block) define_method(:process, &block) end end def initialize(text) @text = text end end @elements = [] def create_element(name, &block) element = Class.new(Element) # Elementの派生クラスを作成 element.module_eval(&block) # blockを新しく作ったクラスのコンテキストで実行 @elements.push [name, element] # @elementsに追加 end # グローバル変数でカウンタ $b_count = 0 $c_count = 0 ## a -- @textを出力するだけ create_element :a do process { puts "<a>#{@text}<a/>" } end ## b -- $b_countをカウントアップして出力 create_element :b do process { count_up puts "<b>#{$b_count}. #{@text}</b>" } def count_up $b_count += 1 end end ## c -- $c_countをカウントアップして出力 create_element :c do process { count_up puts "<c>#{$b_count}.#{$c_count}. #{@text}</c>" } def count_up $c_count += 1 end end @elements.each {|name, element| puts "#{name}:" instance = element.new("#{name} text") instance.process } # ↓count_upメソッドを呼んでみる(エラーになって欲しい) count_up puts "end."
"module_eval"をcreate_elementメソッドの中に隠したい。しかしRuby 1.9だとこのコードはうまく動かない。
・Ruby 1.8での実行結果:
a: <a>a text<a/> b: <b>1. b text</b> c: <c>1.1. c text</c> test.rb: undefined local variable or method `count_up' for #<Object:0x3799c> (NameError)
これは期待通りの動作!
・Ruby 1.9での実行結果:
a: <a>a text<a/> b: <b>0. b text</b> c: <c>0.2. c text</c> end.
Ruby 1.9だとcount_upメソッドがObjectクラスに定義されているので、bのcount_upメソッドがcのcount_upメソッドで上書きされてしまい、$b_countはカウントアップされずに、$c_countが2回カウントアップされてしまう。
processメソッドはdef 〜 endではなくdefine_methodを使って定義しているので、上書きされない。
ではcount_upメソッドもdefine_methodを使って定義すればいいじゃないかと思われるに違いない。確かにそうなのだが、見た目がdefine_methodなのがイヤ。メソッドはやはりdef〜endで定義したい。
というわけで、困った。
- Ruby 1.9では、ブロック付きメソッド呼び出しで受け取ったブロックをmodule_evalに渡すのと、最初からmodule_evalにブロックを渡すのとで動作が異なる
- def〜endの代わりにdefine_methodを使ってメソッドを定義すれば大丈夫
- define_methodは見た目がイヤ。def 〜 endを使いたい
- 直接module_evalを呼べば大丈夫
- module_evalが前面に見えてしまうのはイヤ。他のメソッドの中に隠したい
- (実はすぐにmodule_evalを呼ぶのではなく、Procオブジェクトを溜め込んでおいて後で呼びたいのだけど、それができない)
- def〜endの代わりにdefine_methodを使ってメソッドを定義すれば大丈夫
どうやらRuby 1.9で一度メソッドを経由してから(ブロックをProcオブジェクトに変換してから?)module_evalにブロックを渡すと、Rubyの呼び出し可能オブジェクトの比較 (2) - というよりコンテキストの話とRubyの呼び出し可能オブジェクトの比較 (3) - なんかklassの話で説明されているところの「klass」が書き換わっていない気がします。
こうすればいいんじゃないかーとか、こういう回避策があるよーというった情報があればぜひご教授くださいm(_ _)m
(むしろ今は「Ruby 1.9をブロック付き呼び出し→メソッドにブロック渡しが直接ブロック渡しと同じになるようにパッチを書こう」の方が良いのか…)
※2008/1/9追記:とりあえず回避策を発見しました→Ruby 1.9でmodule_evalとブロック付きメソッド呼び出しの回避策