Tie::Traceで簡単に変数の中身を追うB!

前置き

こんにちは、id:ktatです。最近は、Util::Allというモジュールをいじってますが、CPAN にはあげてないので、紹介できません。

というわけで、今日はデバッグのお供に使えるかもしれない、Tie::Traceを紹介します。

本題

さて、perlのプログラムのデバッグするなら、perl -d というのも良いですが、print デバッグもお手軽でいいですよね。

しかし、怪しい変数を追いかけたり、見知らぬオブジェクトの中を調べたりするのに、いちいち print や warn を挿入していくのも面倒です。

そんな時には、Tie::Trace が役に立つかもしれません。

単純な例

次の $hoge、 @hoge、 %hoge の各変数を追いかけてみます。

use Tie::Trace qw/watch/;

watch my $hoge;
watch my @hoge;
watch my %hoge;

$hoge = 1;

push @hoge, "a";
delete $hoge[-1];

$hoge{1} = 1;
$hoge{"abc"} = "xyz";

以下のようなメッセージが表示されます。

main:: $hoge => 1 at sample1.pl line 7.
main:: @hoge => PUSH('a') at sample1.pl line 9.
main:: @hoge[0] => DELETED('a') at sample1.pl line 10.
main:: %hoge => {1} => 1 at sample1.pl line 12.
main:: %hoge => {abc} => 'xyz' at sample1.pl line 13.

若干分かりにくい表示ですが、main:: 変数名 は、main package で宣言されている変数という意味です。=> の後ろに、代入されたり削除された値が入ります。PUSH や DELETED のように何かしら説明っぽいものが入る場合もあります。

リカーシブに追跡

watch 関数で監視された変数に、ハッシュリファレンスやアレイリファレンスが代入された場合、それらは再帰的にチェックされ、監視する対象になります。

use Tie::Trace qw/watch/;

watch my @array;
push @array, { a => 1 }, [0, 1, 2, [10, 20, 30, { a => 'b'}]];
$array[0]->{b} = 2;
$array[1]->[3]->[3]->{a} = "c";
$array[1]->[3]->[3]->{c} = "d";

としてみます。

main:: @array => PUSH({'a' => 1},[0,1,2,[10,20,30,{'a' => 'b'}]])
 at sample2.pl line 4.
main:: @array => [0]{b} => 2 at sample2.pl line 5.
main:: @array => [1][3][3]{a} => 'c' at sample2.pl line 6.
main:: @array => [1][3][3]{c} => 'd' at sample2.pl line 7.

このように、@arrayに挿入された無名ハッシュや無名配列に値が入ってもメッセージが表示されます。ただし、オブジェクトや tie されたリファレンスなどが入ってきた場合、それらは監視対象にはなりませんので、ご注意ください。

オブジェクトの中身をチェックする

オブジェクトの中身にをチェックするのにも使えます。Mouse(0.40)でやってみました。

package Hoge;

use Mouse;
has "name" => (is =>"rw");
has "height" => (is =>"rw");

package main;

use Tie::Trace qw/watch/;

my $x = Hoge->new;

watch %$x;

$x->name("ktat");
$x->height(173)

以下のような表示がされます。

{name} => 'ktat' at accessor for name (.../Mouse/Meta/Method/Accessor.pm) line 2.
{height} => 173 at accessor for age (.../Mouse/Meta/Method/Accessor.pm) line 2.

package名や変数名が取れていないときは、グローバル変数、無名リファレンスなんかです。

ちなみに、Moose(0.92)だとこんな感じでした。

{name} => 'ktat' at accessor name defined at sample4.pl line 6.
{height} => 173 at accessor height defined at sample4.pl line 7.

にしても、Mouse も Moose もメッセージが独特な感じですね。Mouseの "line 2" がよくわかりませんが、追ってません。

しかし、いずれも代入された箇所がわかりませんね。以下のように、caller オプションを与えてみましょう。

watch %$x, caller => [0, 1];

watch の変数名の後の引数はオプションになります。callerオプションをつけると、指定された分だけたどってくれます。

{name} => 'ktat' at accessor name defined at sample5.pl line 6.
 at sample5.pl line 15.
{height} => 173 at accessor height defined at sample5.pl line 7.
 at sample5.pl line 16.

これで、代入場所が特定できましたね。

リカーシブなチェックをやめる

リカーシブなチェックはいらないという場合もあるでしょう。そういうときは次のようにします。

watch %var, r => 0;

これで、リファレンスが代入されても、それらは監視対象にはなりません。

条件に一致するキーのみ

use Tie::Trace qw/watch/;

watch my %foo, key => ['foo'];
watch my %bar, key => [qr/bar/];
watch my %buz, key => [sub{my($self,$key) = @_;$key =~/buzbuz/}];
watch my %foobarbuz,
  key => ['foo', qr/bar/,
          sub {my ($self, $key) = @_; return $key =~/buzbuz/}];

$foo{foo} = 1;
$foo{hoge} => {foo => 2};
$foo{hoge}->{foo} = 2;

$bar{bar} = 1;
$bar{beer} = 2;
$bar{barbarbar} = 3;

$buz{buz} = 1;
$buz{buzbuz} = 1;

@foobarbuz{qw/foo barbar buzbuz BUZ Buzbuz/} = ('a' .. 'e');

結果は以下のように、key が条件に合致するものしか出力されません。

main:: %foo => {foo} => 1 at sample5.pl line 10.
main:: %foo => {hoge}{foo} => 2 at sample5.pl line 12.
main:: %bar => {bar} => 1 at sample5.pl line 14.
main:: %bar => {barbarbar} => 3 at sample5.pl line 16.
main:: %buz => {buzbuz} => 1 at sample5.pl line 19.
main:: %foobarbuz => {foo} => 'a' at sample5.pl line 21.
main:: %foobarbuz => {barbar} => 'b' at sample5.pl line 21.
main:: %foobarbuz => {buzbuz} => 'c' at sample5.pl line 21.

同じ要領で、key を value に変えると、value に対する条件を指定できます。

メッセージを変更する

表示するメッセージを変えたいこともあるでしょう。例えば、キー 'foo' が入ってきたときの、キー 'var' の値が知りたいとか。

use Tie::Trace qw/watch/;

watch my %foo,
  key => ['foo'],
  debug => sub {my ($self, $v) = @_;
                if (%{$self->storage}) {
                  return $v . " {var} is " . $self->storage->{var}
                } else {
                  return $v;
                }
	      };

%foo = (foo => 1, var => 2);
$foo{foo} = 2;
$foo{var} = 3;
$foo{foo} = 4;

'foo' が代入された行数でのみ、メッセージが出ています。

main:: %foo => {foo} => 1 at sample7.pl line 13.
main:: %foo => {foo} => 2 {var} is 2 at sample7.pl line 14.
main:: %foo => {foo} => 4 {var} is 3 at sample7.pl line 16.

キー 'bar' が入ってきたときの、親のハッシュリファレンスのキー 'xxx' の値が知りたいとか。

use Tie::Trace qw/watch/;

my %parent = (child => {foo => 10, bar => 100}, xxx => "xxx");

watch %parent,
  key => ['bar'],
  debug => sub {
             my ($self, $v) = @_;
             if (my $p = $self->parent) {
               return "$v; parent->{xxx} is ". $p->storage->{xxx};
             } else {
               return $v;
             }
           };

$parent{child}->{bar} = 1;
$parent{xxx} = 10;
$parent{child}->{bar} = 1000;

以下のようなメッセージとなります。

main:: %parent => {child}{bar} => 1; parent->{xxx} is xxx at sample8.pl line 15.
main:: %parent => {child}{bar} => 1000; parent->{xxx} is 10 at sample8.pl line 17.

ここで使われている、storage メソッドは、実際のデータが入っているリファレンスを返し、parent メソッドは親のオブジェクトを返します。

注意事項

たいていのケースで特に問題なく動くとは思いますが、変数を変質させてますので、何か変なことが起きるかもしれません。そういうときは、素直に perl -d でもしてくださいませ。

まとめ

今回は、Tie::Trace を使った、お手軽な print デバッグについて解説しました。

一応日本語のドキュメントも用意してありますので、よろしければ、そちらもどうぞ。

というわけで今回はここまで。明日は id:masartz さんです。