旧聞になってしまいますが、RubyConf2013@Miami beach で発表してきました。
K.Sasada: Object management on Ruby 2.1, Oral Presentation at 13th Inernational Ruby Conference. at Miami beach, Florida on November 9th to 11th of 2013. rubyconf2013-ko1_pub.pdf (1288KB)
なお、すべての発表は http://www.atdot.net/~ko1/activities/ にまとめてあります。
特定のメソッドの呼び出し履歴が欲しい、みたいなケースは時々あるんじゃないかと思います。
というわけで、Ruby 2.0 から導入された Module#prepend (と、一応 keyword 引数)を利用して、そういう履歴を簡単に取得するためのメソッドを書いてみました。
class MethodTracer
class AllTracer
attr_reader :records
def initialize
@records = []
end
def record args, call_site, result
@records << [args, call_site, result]
end
def clear
@records.clear
end
end
class StatisticsTracer
attr_reader :args, :call_sites, :results
def initialize
@args = Hash.new(0)
@call_sites = Hash.new(0)
@results = Hash.new(0)
end
def record args, call_site, result
@args[args] += 1
@call_sites[call_site.to_s] += 1
@results[result] += 1
end
def clear
[@args, @call_sites, @results].each{|col| col.clear}
end
end
def self.trace(cls, mid, statistics: false)
tracer = (statistics ? StatisticsTracer : AllTracer).new
mod = Module.new{
define_method(mid){|*args|
result = super(*args)
tracer.record(args, caller_locations(1, 1)[0], result)
result
}
}
cls.prepend mod
tracer
end
end
class C
def foo(a, b, c)
self
end
end
require 'pp'
tracer = MethodTracer.trace(C, :foo)
#
C.new.foo(1, 2, 3)
C.new.foo(4, 5, 6).foo(1, 2, 3)
#
pp tracer.records
#=>
# [[[1, 2, 3], "t.rb:59:in `<main>'", #<C:0x2b68d64>],
# [[4, 5, 6], "t.rb:60:in `<main>'", #<C:0x2b68cec>],
# [[1, 2, 3], "t.rb:60:in `<main>'", #<C:0x2b68cec>]]
tracer = MethodTracer.trace(C, :foo, statistics: true)
#
C.new.foo(1, 2, 3)
C.new.foo(4, 5, 6).foo(1, 2, 3)
#
pp tracer.args
pp tracer.call_sites
pp tracer.results
#=>
# {[1, 2, 3]=>2, [4, 5, 6]=>1}
# {"t.rb:60:in `<main>'"=>1, "t.rb:61:in `<main>'"=>2}
# {#<C:0x3426834>=>1, #<C:0x3426730>=>2}
MethodTracer.trace に、トレースしたいクラスとメソッドを指定します。
久々に Ruby script をちゃんと書いた。
いや、テストも無いのにちゃんと書いたとか言うな、って感じだろうかのう。
今時だと gist にはったりするんだろうけど、どうやって使うのかわからないという上弱。せめて色づけくらいはしたいのう。
ちなみに、untrace がないのは問題か?
調子に乗って、もう一つ。
今時、キーワード引数に対応していないようなレガシーなメソッドを書く人なんて居ないと思いますが、まぁ、そんなメソッドも時にはあるかもしれません。というわけで、すべてのメソッドをキーワード引数に対応する keyworder メソッドを書いてみました。
def keyworder cls, mid
method = cls.instance_method(mid)
mod = Module.new{
define_method(mid){|**kw|
params = []
method.parameters.each{|(_, param)|
params << kw.fetch(param)
}
super(*params)
}
}
cls.prepend mod
end
class C
def foo a, b, c
p [a, b, c]
end
end
keyworder C, :foo
C.new.foo(a: 1, b: 2, c: 3)
#=> [1, 2, 3]
C#foo は keyword 引数未対応ですが、ちゃんと C.new.foo(a: 1, b: 2, c: 3) のように、キーワード引数で呼べるようになっているのがわかるかと思います。