言語的に云々という話ではなくて、複数人で開発するときにハマりそうなので(というかハマったので)
動的にパッチを当てるには
# gotoするよ版 BEGIN { use UNIVERSAL::require (); if ( Foo->require ) { my $orig = Foo->can('func'); no warnings qw(redefine); *Foo::func = sub { my ($foo) = @_; # shiftしない方が楽なことが多い # pre-process... goto &{$orig}; # post-processが必要な場合は記事の最後で }; } }
とするのが定石(Hook::LexWrap使えっていう話もあるけど本筋とは違うので割愛)だと思いますが、これをどこでやるかという話。
とあるCatalystControllerで変なデバッグ文が出ていて、動的パッチを外しても出続けていて小ハマりした。
調べてみると、他のモジュールでも同じFoo::funcに対して動的パッチを当てていて、そいつがデバッグ文を出力してたというオチだった。つまり、ラップされたFoo::funcだと気付かずに更にラップしていたということだ。
なので、Foo::funcを使用したい箇所で動的パッチを当てるのではなく、MyAppに(もしくはMyApp::ContextをMyApp内でuseしておくなどしてそちらに)書いておく。useでは1度しか読み込まないので1プロセス内で同じパッチを2度当ててしまう事が無くなるというわけ。
もちろんどうしても局所的に当てたいのであれば、ちゃんとlocal *Foo::funcにしておけばあまり問題は無いと思う。(が、そんなことはほとんどないだろう。Net::MSNでちょっと使ったけど)
最後にちょびっとコードの説明を。
- BEGINブロックで行うのは、Foo::funcを使っているBarがuseされてる場合を考えての事。BEGINでやらないと、Barがパッチの当たってないFoo::funcを使ってしまうからね。
- もちろんuse Barより前にパッチを当てないといけないからuseする順番を気にしなきゃいけない。
- で、それは面倒なので、PERL5OPT=-MMyApp::Contextしちゃう方がいいのかもしれない。
- もちろんuse Barより前にパッチを当てないといけないからuseする順番を気にしなきゃいけない。
- *Foo::func{CODE}じゃなくてFoo->can('baz')を使っているのは、funcがFooではなく先祖クラスで定義されてても気にしなくて済むから。
- $orig->(@_)の部分、wantarrayを見てそのコンテキストで戻り値を受けてreturnしないとちゃんと動かないよ。
上のコードにはそこまで書かなかったけど。一応書いた↓
そうです(多分読み間違ってるんだと思いますが、自分が勘違ってる可能性もありますので一応書いておきます)
goto &{$orig}の場合は、最初に書いたコードの通り(そしてid:tokuhiromが書いた通り)wantarrayは見なくて良いのです。
以下のコードはpost-processを行うためにgoto &{$orig}せずに$orig->(@_)しているので、wantarray周りを明示的に処理してます。
# post-processを行うためにgotoしないよ版 BEGIN { use UNIVERSAL::require (); if ( Foo->require ) { my $orig = Foo->can('func'); no warnings qw(redefine); *Foo::func = sub { my ($foo) = @_; # shiftしない方が楽なことが多い # pre-process... my @r; if ( !defined wantarray ) { $orig->(@_); } elsif (wantarray) { @r = $orig->(@_); } else { $r[0] = $orig->(@_); } # post-process... return wantarray ? @r : $r[0]; }; } }
Foo::funcがwantarray見てごにょごにょやってることを考えると、voidコンテキストかどうかも調べないといけない。