TracePointのenable(target: )は戻り値を書き換えるときべんり


この記事は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#enabletarget引数のユースケースとしてメソッド呼び出しの戻り値を書き換えるときガード条件を書かずに書き換えられてべんりな例を示しました。
また、Refinementsと組み合わせることでRubyで定義されていない、Cで定義されたメソッドの戻り値も書き換えることができることを示しました。
:returnイベント以外に:callイベントにも対応しているので、メソッド呼び出し時に何かしたいときにも応用できると思います。
リリースが楽しみですね。

TracePoint悪用してカジュアルに戻り値書き換えていきましょう