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 を得る方法は無いのでしょうか?
Rails(ActiveSupport) にはかつて、その名もまさしく、 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
みたいにすると分け分かんなくなっていいんじゃないですかね。