godscope.rb の解説

今日 mongo のソースの GodScope というクラスをみて、Rubyデバッグ時に

  1. private メソッドを public にする
  2. インスタンス変数の accessor を定義する

というのを一括でやりたいと思うことがたまにあるなーと思い出して、ちょっと書いてみました。 Ruby は private なメソッドも Object#send を使って呼べるしインスタンス変数も Object#instance_variable_(get|set) で操作できるので必須ではないんですけど、irb からプロセスの内容を覗いてみる時などタイプ量が増えて面倒だから accessor 定義しちゃえーとかよくするので。

とりあえず Gist にしたので貼りつけます。

https://gist.github.com/1681013

解説

でまあこれで終わりでもいいんですけど、Ruby に慣れている人はフンフンと読めちゃうと思いますが、何をしているのかわからんという人に向けて解説書いてみることにします。あ、テクニック的な部分はほぼ メタプログラミングRuby にも書いてあることだけだと思うので メタプログラミングRuby を読んだ人はフンフンと読んでいってください。

全ての private メソッドを public にする

では上から順番に。まず最初のブロック

ObjectSpace.each_object(Module) do |mod|
  mod.private_instance_methods(false).each do |meth|
    mod.__send__(:public, meth)
  end
end

ここでは全ての Module/Class の private なインスタンスメソッドを public に変更しています。

ObjectSpace.each_object(Module) do |mod|
  ...
end

ObjectSpace.each_object do ... end はプロセス内の全てのオブジェクトについてブロックを呼び出すメソッドです。引数にモジュールやクラスを指定すると、そのモジュールやクラスと Object#kind_of? の関係にあるオブジェクトだけを取り出すようになります。ここで Module を引数に渡しているということは、mod.kind_of?(Module) が真になるような mod の全てを対象にしたいということです。 Module はクラスですから、つまり Module のインスタンスか、Module を継承したクラスのインスタンスということになります。Module を継承しているクラスは組み込みクラスでは Class だけです。つまりここでは 「全てのモジュールとクラスについて ... を実行する」ということをしています。

  mod.private_instance_methods(false).each do |meth|
    ...
  end

次はブロックの中身です。 mod にはモジュールかクラスが入っています。 Module#private_instance_methods(false) を使って、そのモジュール/クラスで定義されている private なインスタンスメソッドの名前の Symbol *1の配列で返します。引数に false を指定すると、そのモジュール/クラスで直接定義されているメソッドのみを返し、継承しているクラスや include しているモジュールで定義されているものは含まないようにしています。実はこの後で使っている Module#public は継承したメソッドについても利用できるのですが、今は全てのモジュールとクラスについて同じ操作をしていますから、継承したメソッドについては元々定義されているところで処理されるはずですので重複しないよう省きます。その後の each はいいですね。取り出した private なインスタンスメソッドの名前の Symbol のそれぞれについて "..." を実行します。

    mod.__send__(:public, meth)

mod はモジュールかクラスで meth は private なインスタンスメソッドの名前の Symbol です。 Object#__send__ を使って Module#public を呼び meth のメソッドを public に変更しています。[追記]Object#send でもいいのですが、send が再定義されている場合を考慮して Object#__send__ にしました。たるいさんご指摘ありがとうございます[/追記]
Module#public はモジュールやクラスの定義でまるで宣言や構文のように使われることが多いですが、これは Module のインスタンスメソッドです。

class A
  def a
  end
  public :a   # => 実は A.public(:a) のようなメソッド呼び出し
end

ただし Module#public 自体が private メソッドなので、実際には A.public(:a) のようにレシーバを指定した呼び方はできません。なので Object#__send__ を使って private メソッドを呼ぶ方法を利用しています。

ここで2つほど考慮していない点があります。

  1. protected メソッド
  2. 特異メソッド/クラスメソッド

protected メソッドってありますよね。でもあんまり使わないのでここでは無視しています。 protected メソッドも public にしたければ protected_instance_methods で取り出して Module#public を呼べばいいでしょう。
特異メソッドについても無視しています。ObjectSpace#each_object(klass) は klass と kind_of? の関係にあるオブジェクトを列挙すると書きましたが、実は特異クラスについては skip されます。特異メソッドは特異クラスに定義されるので、上記の実装では特異メソッドは public にする対象に含まれません。特異メソッドは通常 public (レシーバを指定して呼ぶ)と思うので無視しています*2

インスタンス変数の accessor を定義

では次にインスタンス変数の accessor を定義する部分です。

class Object
  def method_missing(meth, *args)
    ...
  end
end

Object#method_missing を定義することで、未定義のメソッドが呼ばれた時にそれが accessor として妥当だったらその時点で accessor を定義する、という方針で行きます。 Rubyインスタンス変数はクラス定義から自明なものではなくて、インスタンス(オブジェクト)毎に実際にインスタンス変数への代入が行なわれた時に作られるからです。従って同じクラスのインスタンスである2つのオブジェクトが持っているインスタンス変数の名前が違うということがありえます。

    if /^(.+)=$/ =~ meth.to_s and args.size == 1
      name = $1.to_sym
    elsif args.empty?
      name = meth
    else
      return super
    end

Object#method_missing の第1引数(meth)には呼ばれたメソッド名が渡されます。まずこのメソッド名が "aaa=" のように最後が "=" になっているかどうかをチェックします。またこの時引数(args)の数は1つであるはずなのでそれもチェックしています。メソッド名が "aaa=" の時、それを accessor として解釈するとインスタンス変数 @aaa への代入と見做せますから、 "=" を除いた "aaa" の部分だけ name に取り出しておきます。 "aaa=" のような形でなかった時は、インスタンス変数の参照かもしれないと考え、その時は引数が空のはずなので args.empty? でチェックします。これらの条件にあてはまらなかったらそれは accessor の呼び出しとは見做せないので return super することで NoMethodError を発生させています。これ return はいらないかもしれませんが、念のため。

    if self.instance_variable_defined?("@#{name}")
      self.class.__send__(:attr_accessor, name)
      if meth == name
        self.instance_variable_get("@#{name}")
      else
        self.instance_variable_set("@#{name}", args[0])
      end
    else
      super
    end

メソッド呼び出しが accessor っぽいと判断したら実際に対応する名前のインスタンス変数が存在するか Object#instance_variable_defined? でチェックします。存在しなければ(else 節) super として NoMethodError を発生させます。存在したら、まず self.class でそのオブジェクトのクラスを取得して、Module#attr_accessor を呼ぶことで accessor を定義させています。ここでも Module#attr_accessor が private メソッドなので Object#__send__ を使って呼んでいます。これで次回からは method_missing は呼ばれずにここで定義された accessor が呼ばれるようになります。でもとりあえず最初に呼ばれた時の処理をここでするために Object#instance_variable_(get|set) を使ってインスタンス変数の参照、代入を行なっています。
[追記]attr_accessor を呼んで定義を追加するのはやめました。@a があってメソッド a が定義済みで a= を呼ばれると a を上書きしてしまいます。reader/writer の必要なほうを定義すればいいのですが、まあどうせ抜け道なので毎回 method_missing で処理するのでよいということにしました。[/追記]

なお、instance_variable_defined? で引数に文字列を渡していますが、これはわざとです。 instance_variable_defined? を呼んでいる時点ではまだそのインスタンス変数の名前が実際に存在するかどうかわかりません。今の trunk(2.0) の ruby では instance_variable_defined? のような Symbol を受け付けるメソッドに String を渡した時に、その Symbol が未定義ならそもそも Symbol 化せずに false を返すようなショートカットが実装されているので、ここで to_sym を呼んで Symbol 化してしまうと、不要な Symbol が生成されてしまうからです。
それから、今の実装は accessor をそのオブジェクトの特異メソッドとしてではなくてクラスのインスタンスメソッドとして追加するようにしています。先程書いたように同じクラスのインスタンスでもインスタンス変数が異なる場合があるので、これだとそのオブジェクトには存在しないインスタンス変数の accessor もありえるのですが、そもそも attr_accessor を使う時も必ずしもそのインスタンス変数が最初から存在するとは限らないのであまり問題はないだろうということと、もし特異メソッドを使って accessor を定義してしまうと、そのオブジェクトは Marshal.dump で dump 不可能になってしまうので、dRuby を多用しているうちの環境では困ることが多いだろうという判断からこうしています。

こんな感じです。思ったより長くなってしまいました。今気がつきましたが普段ここを読んでいるような人には常識的なことばかりなので、どこか別のところに書けばよかった! るびまに投稿するとか。

*1:1.9 の場合。1.8 だと文字列だったと思います

*2:別の特異メソッドから呼ぶために private な特異メソッドを定義するということは考えられますが