読者です 読者をやめる 読者になる 読者になる

Binding+callerで作るデバッグ出力メソッド

Rubyで変数をデバッグ出力をするときに、

hoge = 100
puts "hoge = #{hoge}"

とかやるとおもうんですが、いちいち "hoge = #{hoge}" って書くのは面倒ですよね。
なので、理想的には

hoge = 100
debug hoge # => 'hoge = 100' (あくまで理想)

となってくれればありがたいです。ではどうしたらこのようなdebugメソッドが作れるでしょうか。

変数名をdebugメソッドに伝える

Rubyをやっている人なら、ちょっと考えれば分かると思うのですが、上記の文面のままでは無理です。なぜなら、メソッドdebugへは hoge の「値(100)」が渡されるので、debug の中で 'hoge' という「名前」を得る事が出来ないからです。なので、

debug hoge   # (1) 無理
debug 'hoge' # (2) これならできそう
debug :hoge  # (3) 2よりちょっとだけスマート

(3)の路線で行こうと思います。

Bindingの登場

それでは debug の中身に入っていこうと思います。
以下のような実装を思いつく方がいるかもしれません。

def test
  hoge = 100
  debug :hoge
end

def debug(var_name)
  puts var_name.to_s + ' = ' + eval(var_name.to_s)  # 誤り
end

test

ですがこれは誤りです。なぜかというと、 debug の中では、メソッド test の中で使われている変数 hoge にアクセスできないため、eval の部分で「NameError: undefined local variable or method」になるはずです。
ところが Ruby には、このような debug の中から test のスコープ(コンテキスト)にアクセスする手段があります。それが Binding です。

Kernel#binding メソッドを用いると、 Binding 型のオブジェクトが取得できます。Binding オブジェクトは、コンテキストを格納したオブジェクトです。Ruby はコンテキストでさえオブジェクトなのです!
Binding オブジェクトのコンテキストは、Binding#eval メソッドによってアクセスすることができます。

def func
  msg = 'hello'
  binding
end

context = func
puts context.eval('msg')  # => 'hello'
メソッド呼び出し元のBindingを取得できるか?(Binding.of_caller問題)

Binding が理解できたところで、もう一度、 debug を見てみましょう。

def test
  hoge = 100
  debug :hoge
end

def debug(var_name)
  # なんとかして test の binding を得て、hoge をダンプする
end

test

では、どうしたら debug の呼び出し元である test の binding を得られるでしょうか。
一つの回答は、 debug の引数として binding を与える事です。

def test
  hoge = 100
  debug :hoge, binding  # debugの引数にbindingを渡す
                        # => 'hoge = 100'
end

def debug(var_name, context)
  context.eval('puts "'+var_name.to_s+' = #{'+var_name.to_s+'.inspect}"'
end

test

ですが、 binding を引数に取るのはあまりスマートではないように見えるかもしれません。
なんとかして debug の中からスマートに呼び出し元(caller)の binding を得る方法は無いのでしょうか?

RailsActiveSupport) にはかつて、その名もまさしく、 Binding.of_caller というメソッドがありました。これは set_trace_func(のバグ)や callcc(継続) などを駆使した黒魔術的な実装になっており、大変便利なものだったのですが、set_trace_func のバグ修正により ruby 1.8.5 から動かなくなりました。
今回のエントリでやろうとした、「hoge = 'hoge'」なデバッグメソッドは、実は るびきち さん の ppp というライブラリで実現されているのですが、 ppp では内部的に Binding.of_caller を使っているため、最近の ruby では動きません。

なので Binding.of_caller を作ろうと思ったのですが、そのために必要な set_trace_func はスレッドセーフでないので、サーバサイドのプログラムには向かないし、正直、黒魔術過ぎて俺の手には負えませんでした。

なのでこのへんが今回の着地点です。

def test
  hoge = 100
  binding.debug :hoge  # => 'test.rb:3:in `test': hoge = 100'
end

class Binding
  def debug(var_name)
    self.eval('puts "'+caller(1).first+': '+var_name.to_s+' = #{'+var_name.to_s+'.inspect}"')
  end 
end

test

debugをBindingのメソッドにしちゃいました!!
正直、 binding を引数に与える場合と汚さは変わらないですね。
ruby1.9 で動くこと、スレッドセーフな点が売りです。caller を付けてみたので呼び出し元のファイル名や行番号、メソッド名が表示されます!

ここまで読んでしまった人、残念でした。1.9で動くスレッドセーフな Binding.of_caller を作って俺に送ってください。

追記

binding って書くのがイヤな人は

class Binding
  def out(var_name)
    self.eval('puts "'+caller(1).first+': '+var_name.to_s+' = #{'+var_name.to_s+'.inspect}"')
  end 
end

alias :debug :binding

hoge = 100
debug.out :hoge

みたいにすると分け分かんなくなっていいんじゃないですかね。