読者です 読者をやめる 読者になる 読者になる

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オブジェクトを溜め込んでおいて後で呼びたいのだけど、それができない)

どうやら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とブロック付きメソッド呼び出しの回避策