Probe Object

ハッカーと画家の中で、「これからの言語はプロファイラが重要だ」に影響されて、おもいついたネタ。
Rubyでは動的型付けの悪影響で、ライブラリなんかの関数に何のオブジェクトを渡せばいいのかさっぱりわからないことがある。ソース読んでも関数定義だけでは仮引数の個数と名前くらいしかわかんない。

  • 変数宣言に型を書かなくていいなんて、なんて書くのが楽なんだ、ビバRuby!
  • 変数宣言に型を書かなくていいなんて、なんて読むのがつらいんだ、ガッデムRuby!

てなかんじ。
そこで僕なんかは、Rubyコードの型推論をやってくれちゃうid:soutaroさんのTypingRuby(http://truby.sourceforge.jp/index.j.html)なんかに期待してしまうわけだが、今回は逆のアプローチを思いついたよ!というお話。
以下長いので、ここでおる。
実行時にいろいろやるのが得意なRubyなんだから、とにかく走らせてみればいいのだ。何をわたすべきかわかんない関数でも、オブジェクトを渡して実行しちゃう。んでもって、そのオブジェクトは、自身に対して呼ばれたメソッド名を自分で記録するような仕組みをしこんでおく。どうせどっかで例外なりエラーなりが起こるだろうから、そのときにオブジェクトの中身を見てやれば、少なくともそのオブジェクトに期待されているメソッドがわかるんじゃなかろか?

と思って、そういう働きをするものをProbe Objectと名づけてささっとスケッチしてみた。こんな感じ。

module Probe_mod
  def initialize
    @list = {}
    @name = self.class.to_s
  end

  def __list
    @list
  end

  def __def_class(newclassname)
      eval <<EOS
class #{newclassname}
  include Probe_mod
end
EOS
  end

  def method_missing(name,*arg)
    if @list[name] == nil
      newclassname = "#{@name}_#{name.to_s}"
      newobj = nil
      __def_class(newclassname)
      eval("newobj = #{newclassname}.new")
      @list[name] = [newobj,[]]
    end
    @list[name][1] << arg.map {|item| item.class}
    return @list[name][0]
  end
end

class Probe
  include Probe_mod
end

Probeオブジェクトは、さっきも書いたように自分に対して呼ばれたメソッド名を保持している。このへんmethod_missingを使ってどういうメソッドが呼ばれても反応するようにしてある。どう反応するかというと、「Probe_呼ばれたメソッドの系列」 というクラスを新たにつくって、そのインスタンスを返す。自分は呼ばれたメソッド名をキーに返り値用に作ったインスタンスと、渡された引数のクラス名を保持する。
つまり、Probeオブジェクトの中身を見れば、

  1. どのようなメソッドが呼ばれたか
  2. そのときの引数に渡されたクラスは何か?
  3. さらにそのメソッドの返り値がどう使われるのか?

がわかるようになってる。最後のがわかるのは、再帰的にProbeオブジェクトを返してるからだ。ちなみに、例外が起こるときにクラス名が提示されることが多いようなので、クラス名を見るだけでどういうメソッド呼びだし系列で生成されたオブジェクトなのかわかるようにしてみた。

使い方はこう。あまりいいサンプルが思いつかなかったので、webrickでサンプルのためのサンプルを作ってみた。

require 'webrick'
s = WEBrick::HTTPServer.new(:Port=>2000)
test = Probe.new
s.mount('/test',test)
require 'pp'
trap("INT"){
  pp test
  s.shutdown
}
s.start

Webrickの動かし方(startするとか)は知っているのに、mountの第二引数に何を渡せばいいかわかんないケース。ちょっと無理な仮定だ。無理無理だ。ごめんなさい。
これを走らせるとWebrickはサーバとして動作するので、localhost:2000にアクセスしてみる。何もいわれない。適当なとこでCtrl-Cでwebrickをストップさせると、Probeオブジェクトの中身がppでざざっと表示される。
こんなかんじ。

#<Probe:0x402a23c8
 @list=
  {:get_instance=>
    [#<Probe_mod::Probe_get_instance:0x40335c4c
      @list=
       {:service=>
         [#<Probe_mod::Probe_get_instance_service:0x40335904
           @list={},
           @name="Probe_mod::Probe_get_instance_service">,
          [[WEBrick::HTTPRequest, WEBrick::HTTPResponse],
           [WEBrick::HTTPRequest, WEBrick::HTTPResponse]]]},
      @name="Probe_mod::Probe_get_instance">,
     [[WEBrick::HTTPServer], [WEBrick::HTTPServer]]]},
 @name="Probe">

ちょっとみづらいが、@listがハッシュになってて、もろもろの情報がはいってる。
これを見るとまずわかるのが、Probeオブジェクトのget_instanceというメソッドが呼ばれて、その時の引数はWEBrick::HTTPServerだということ。
でもって、そのとき返したオブジェクトはProbe_get_instanceという名前のクラスのインスタンスになるので、その中身をみてやる。こいつの@listも同じように解釈できて、serviceというメソッドが、WEBrick::HTTPRequestとWEBrick::HTTPResponseという引数で呼ばれていることがわかる。
さらに、serviceメソッドの返り値で渡したProbe_get_instance_serviceオブジェクトの@listをみると、これは空だ。たぶん、なにも使われてないんだろう。
ここまでで、だいたいWEBrick::HTTPServerの挙動は想像できる。

  • get_instanceが呼ばれるのでシングルトンを期待している。つまり本来mountの引数に渡すのはクラスだろう。
  • get_instanceで生成されるインスタンスに対して、serviceメソッドが呼ばれる。
  • serviceメソッドの引数はRequestとResponseが渡される。いかにもserviceメソッドの中でRequestの情報を使ってResponseのbodyをほげほげしてやれば動作しそう。

本来はmountの引数にはWEBrick::HTTPServlet::AbstractServletを継承したクラスを渡すことになっているのだが、シングルトンのところは無視して*1上の予測に従って、Probeをこう書きかえてやると、、

class Probe_Dummy
  include Probe_mod

  def get_instance(server)
    @server = server
    return self
  end

  def service(req,res)
    res.body = 'hoge'
  end
end

なんとlocalhost:2000にアクセスすると、ちゃんとhogeと表示されてしまう。ブラボー!Duck Typing!
もうちょっと考えると、requireでとりこんだクラス群のメソッド総チェック、なんてのにも使えそう。新規に追加されたクラスをかたっぱしからnewして、もってるpublicメソッドにこのProbeをつっこんで実行させまくる、とか。
問題は例外が発生したときにどうするか、かなあ。
それに、サンプルとしてwebrickはちょっと特殊すぎるので、効果がいまいち疑わしい。サンプルとして使えそうなライブラリを思いついた方はコメントお願いします。
あれ…どこがプロファイラなんだろう…

*1:ちゃんとソースを見ると、シングルトンじゃなくて単にインスタンスを生成してるだけだった。シングルトンは嘘でした。