動的パッチは一つのモジュールにまとめてuseした方が良いかも

言語的に云々という話ではなくて、複数人で開発するときにハマりそうなので(というかハマったので)


動的にパッチを当てるには

# 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しちゃう方がいいのかもしれない。
  • *Foo::func{CODE}じゃなくてFoo->can('baz')を使っているのは、funcがFooではなく先祖クラスで定義されてても気にしなくて済むから。
  • $orig->(@_)の部分、wantarrayを見てそのコンテキストで戻り値を受けてreturnしないとちゃんと動かないよ。上のコードにはそこまで書かなかったけど。一応書いた↓
http://d.hatena.ne.jp/tokuhirom/20090601/1243824392
そうです(多分読み間違ってるんだと思いますが、自分が勘違ってる可能性もありますので一応書いておきます)
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コンテキストかどうかも調べないといけない。