この記事はOkinawa.rb Advent Calendar 2018の4日目の記事です。
昨日は @hanachin_ さんのRe: シクシク素数アドベントカレンダー Ruby 編でした。
明日は @hanachin_ さんのpendingしてからコミットpushしよう 〜RSpecしぐさ〜です。
TracePoint#enableのtarget引数
Ruby 2.6ではTracePoint#enable
にキーワード引数でtarget
を渡せるようになりました。
https://bugs.ruby-lang.org/issues/15289
例えばTracePointでreturnする際に戻り値を書き換えたいとき「書き換えたくないメソッドの戻り値も書き換えちゃわないかな〜? method_id
とかself
とか行番号とか確認してガードしとかないとな〜〜〜。」みたいなことを心配しなくてよくなります。
targetに有効にしたい対象を渡すだけ!そのtargetに対してのみTracePointが実行されるとわかっているので安心して戻り値を書き換えることができますね!
べんり!
ソースコード
とくにいいお題も思いつかなかったのでFizzBuzzで確認することにしました。
# 2.6.0-devで動く
using Module.new {
refine(Object) do
def fizzbuzz_env(*ns)
Module.new {
# このRefinementsのモジュールをあとで使うので取っておく
r = refine(Integer) {
# methodがRubyで定義されていないとenableのtargetに指定できないため
# Refinementsで再定義
def to_s
super
end
}
refine(Object) {
# fizzbuzzの環境ではメソッド名が数字のメソッドを定義しておく
# 以下はfizzbuzz, fizz, buzzに対応したメソッドの定義
ns.each do |n|
if n % 3 == 0 && n % 5 == 0
define_method(:"#{n}") { "fizzbuzz" }
elsif n % 3 == 0
define_method(:"#{n}") { "fizz" }
elsif n % 5 == 0
define_method(:"#{n}") { "buzz" }
end
end
# 普通の数字はmethod_missingで拾う
def method_missing(name, *)
return super unless name.match?(/\A\d+\z/)
name.to_s
end
# 普通にputs 1と呼ぶとRefinementsで定義したto_sが呼ばれない、to_sを呼ぶ処理を入れる
def puts(*args)
super(*args.map(&:to_s))
end
define_method(:fizzbuzz) { |&block|
# このTracePointはenableするときにtargetが絞られている
# Integerのto_sにしか反応しないので色々なガード条件を考える必要がない! べんり!
# ガード条件を1つも書かずに雑に書き換えてOKです
rewrite = TracePoint.new(:return) { |tp|
tp.return_value[0..-1] = __send__(tp.return_value)
}
# Integer.public_instance_method(:to_s)だとrefineしたメソッドが取れない
# また、Cで書かれているメソッドはtargetに指定できない
# なので先程のRefinementsの中でRubyで定義したto_sをr経由で参照する
# このfizzbuzzメソッドにわたしたブロックの中でIntegerをputsしたりto_sすると
# fizzbuzz化した文字がかえる。
rewrite.enable(target: r.public_instance_method(:to_s), &block)
}
}
}
end
end
}
using fizzbuzz_env(*1..100)
# fizzbuzz化した文字がでる
fizzbuzz do
puts *1..16
end
puts "-" * 16
# こっちは普通の数字がでる
puts *1..16
puts "-" * 16
# ふつうの文字はfizzbuzz化しない
fizzbuzz do
puts "1"
end
実行結果
% ./ruby -v --disable-gems /tmp/tp.rb
ruby 2.6.0dev (2018-12-04 trunk 66199) [x86_64-linux]
1
2
fizz
4
buzz
fizz
7
8
fizz
buzz
11
fizz
13
14
fizzbuzz
16
----------------
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
----------------
1
まとめ
TracePoint#enable
のtarget
引数のユースケースとしてメソッド呼び出しの戻り値を書き換えるときガード条件を書かずに書き換えられてべんりな例を示しました。
また、Refinementsと組み合わせることでRubyで定義されていない、Cで定義されたメソッドの戻り値も書き換えることができることを示しました。
:return
イベント以外に:call
イベントにも対応しているので、メソッド呼び出し時に何かしたいときにも応用できると思います。
リリースが楽しみですね。
TracePoint悪用してカジュアルに戻り値書き換えていきましょう