module_evalの落とし穴

http://d.hatena.ne.jp/mokehehe/20071113/evalでmodule_evalの中のクラス変数の扱いで困ってるようだったのでコメントしてみた。確かにmodule_evalにブロックを渡して評価するコンテキストをすげかえた時の、クラス変数参照/代入の挙動ってよくわからない。以前も動的にクラス生成したい時にクラス変数の扱いではまって結局違う方法で逃げたような記憶だけある。
このへんの挙動ってどっかに書いてあるのかなあ、とリファレンスマニュアルみてみたら、module_evalの項にちゃーんと書いてありました。

ブロックが与えられた場合にはそのブロックをモジュールのコンテキスト で評価してその結果を返します。ブロックの引数 mod には self が渡されます。
モジュールのコンテキストで評価するとは、実行中そのモジュールが self になるということです。つまり、そのモジュールの定義文の 中にあるかのように実行されます。
ただし、ローカル変数は module_eval の外側のスコープと共有し ます。ruby 1.6 feature: version 1.6.8 以降でブロックが与えられた場合は、定数とクラス変数 のスコープも外側のスコープになります。

http://www.ruby-lang.org/ja/man/?cmd=view;name=Module

な、なるほど…。定数とクラス変数も外側のスコープと共有されるのか。instance_evalの場合はローカル変数だけが外側のスコープと共有されるらしい。そういえばmodule_evalの中で定数に代入した時の挙動もなにか妙だったような記憶がある。そういうことだったんだなあ。奥が深い…。
いやしかし、ローカル変数さえ外側のスコープと共有してくれれば、クラス変数も定数も内側のコンテキストで評価してくれるほうが嬉しい気がするんだけど、なんでこうなってるんだろう?

追記 ちょいまとめと新たな疑問

動的に(クラス変数を持った)クラスを生成するつもりで、

A = Class.new {
  @@c_var = 1
  def foo
    @@c_var
  end
}

p A.new.foo         # => 1
p A.class_variables # =>["@@c_var"]

こういうソースを書くと、期待した値も返ってくるし、動的に生成したクラスAのクラス変数の一覧にも登録されてるので、意図通り動いているようにみえる。が、実はそれは間違い。
続けてこのコード、

p Object.class_variables # => ["@@c_var"]

を実行するとAと同じ結果が返ってきてしまう。なんと動的に生成したAに登録したつもりのクラス変数が、Objectクラスのクラス変数に設定されてしまっている。クラス変数はサブクラスからも参照可能なので、Objectクラスに設定されたクラス変数がクラスAでも見えていた、というのが真相。
どうしてObjectクラスに設定されてしまったかというと、Class.newに与えたブロックの中での@@によるクラス変数へのアクセスは、外側のスコープ、つまりこの例だとトップレベルのスコープで行われるから。トップレベルのselfは、mainというObjectクラスのオブジェクトなのでトップレベルでクラス変数を設定すると、Objectクラスにクラス変数が設定されてしまう、というわけ。

一方、メソッド呼出しは外側のスコープじゃなくて内側のスコープ(つまり動的に生成したクラス)になるので、@@で直接代入するのをあきらめて、Moduleクラスのメソッドであるclass_variable_setメソッドを使ってみる。

A = Class.new {
  class_variable_set(:@@c_var,1)
  def foo
    @@c_var
  end
}
p A.class_variables       # => ["@@c_var"]
p Object.class_variables  # => []
p A.new.foo               # => in `foo': uninitialized class variable @@c_var in Object (NameError)

ひとまずクラス変数の設定には成功しているようだが、参照するときにやっぱりObjectクラスのクラス変数を探しにいっている。さっきと同様に「外側のスコープ」のルールが効いてるみたい。しかたなく、class_variable_getを使おうとするが、defの中のスコープは「動的に生成したクラスA」ではなく、「動的に生成したクラスAから生成するオブジェクト」のスコープになっているので、self.classを介してアクセスしないといけない。ところがところが、class_variable_getはプライベートメソッドなのでself.class.class_variable_get、という形式ではアクセスできない。プライベートメソッドまで呼びだせる最後の手段sendを使って、

A = Class.new {
  class_variable_set(:@@c_var,1)
  def foo
    self.class.send(:class_variable_get,:@@c_var)
  end
}
p A.class_variables      # => ["@@c_var"]
p Object.class_variables # => []
p A.new.foo              # => 1

としてやるとようやく成功する。

あれでも、ブロックの中のさらにdef定義文の中ならさすがに"@@"を使ってそこのスコープでクラス変数にアクセスできてもいいんじゃないかな。

a = 1
A = Class.new {
  def foo
   a
  end
}
p A.new.foo # => `foo': undefined local variable or method `a' for #<A:0xb7ca1b58> (NameError)

と書くとエラーになる、つまりローカル変数さえdefの内側とブロックの外側ではスコープを共有しないみたいなのに。うーむ。