#japanpmのおかげで、最近コードを書けてる。その中で、attribute周りをいじっていたので、そのことについて書きたいと思う。
Perlでattributeを使う時、普通、Attribute::Handlersを使って書くと思う。
package AttrSample; use Attribute::Handlers; our @INFO; sub Stock :ATTR(CODE) { my ($package, $symbol, $referent, $attr, $data) = @_; push @INFO => { code => $referent, some => 'info' }; } 1; package AttrUser; use parent qw(AttrSample); sub hello :Stock() { } # => stocked `hello` at INFO
しかし、自分の場合、次の問題点を感じた。
- AttrSampleを動的読み込みした場合、Stock attributeは呼び出されない
- Attribute::Handlersのデフォルトだと、attributeはCHECKブロックで呼び出されるが、CHECKブロックは遅延読み込みした際、発火されなかったりする
- Stock attribute実行以前だと、INFOに情報がストックされない
- attributeを指定していたら、全てストックされている方が良い
- 継承が汚れる
- Attributeを利用できるようにしたいだけであって、継承したいわけじゃない
- Exporterとの食い合わせで、意図せぬ結果になってハマる(ハマった)
やりたいことを補足すると、自分の場合、詰まるところ、perl -wc
によるチェックで問題に気づきやすくしたい。Perlの動的な性質上、静的解析でできることには限度があると思う。かといって、全てのコードを実行してからでは問題に気づくのは遅い。だから、コンパイル時までのチェックを強化すると良いと思ってる。この実現のためには、チェックに必要な情報をコンパイル時までに揃える必要があるが、attributeやkeyword pluginあたりのギミックが都合良い。この記事では、attributeで、コンパイル時までにチェックに必要な情報を揃えるためにどうすると良さそうか書く。
結果としては、次の形に落ち着いた。気になる点があれば、コメントをもらえると嬉しい。より具体的なコードはFunction::Returnにある。
use Attribute::Handlers; use B::Hooks::EndOfScope; my @ARGS; sub import { my $class = shift; my $target = caller; { # ③ attributeを、$targetで利用できるように、 # MODIFY_CODE_ATTRIBUTES, _ATTR_CODE_Stockを差し込む no strict qw(refs); my $MODIFY_CODE_ATTRIBUTES = \&Attribute::Handlers::UNIVERSAL::MODIFY_CODE_ATTRIBUTES; *{"${target}::MODIFY_CODE_ATTRIBUTES"} = $MODIFY_CODE_ATTRIBUTES; *{"${target}::_ATTR_CODE_Stock"} = $class->can('Stock'); } # ② 詰め込んだattributeの引数を、コンパイル時に一気に処理 on_scope_end { while (my $args = shift @ARGS) { # ... do something by $args } }; } # ① BEGINフェーズで、@ARGSにattributeの引数を詰め込んでおく sub Stock :ATTR(CODE,BEGIN) { push @ARGS => \@_; return; }
やっていることは、大きく3つ。①②によって、CHECKブロックでなく、importのコンパイル時に一気に処理。③によって、継承を汚すことなく、指定のパッケージでattributeを利用できるようになる。
詳解
importのコンパイル時に一気に処理
改めて、処理の流れは次の通りで、ここではこの処理の背景を書く。
- ① BEGINフェーズで、@ARGSにattributeの引数を詰め込んでおく
- ② コンパイルフェーズに、詰め込んだattributeの引数を元に一気に処理
まず、CHECKブロックは、requireやevalなどで遅延読み込みされた際、呼び出されないことがある。*1その回避のために、ATTR(CODE,BEGIN)
でBEGINフェーズでの実行と、B::Hooks::EndOfScope#on_scope_endを組み合わせる。on_scope_endは、スコープのコンパイルが終わった時にコード実行してくれる君。この2つの組み合わせを素直に実装すると次のようになると思う。
sub Stock :ATTR(CODE,BEGIN) { on_scope_end { push @INFO => { code => $referent, some => 'info' }; } }
しかし、このコードには問題がある。 具体的には、INFOを取り出す処理より後にINFOをStockすると、INFOが期待通り取り出せない。 目的はチェックに必要な情報を全て整えることなので、一部情報が欠けるのは問題になる。
package AttrUser; use parent qw(AttrSample); use B::Hooks::EndOfScope; use DDP; sub import { on_scope_end { p @AttrSample::INFO; # => EMPTY!! # XXX: 本当は、\&helloに関する情報が欲しいが、`sub hello :Stock`はコンパイルまだされていないので、INFOは空 } } sub hello :Stock() { } 1;
結局、attributeごとにon_scope_endを実施するのはやめ、以下コードのように、importに集約して一気に処理する形にする。こうすることで、チェックに必要な情報を揃えやすくなる。もしチェックに必要な情報が取得できないとしたら、このサンプルコードでいうところのAttrSample#importのコンパイルよりも前に取り出そうとしている、と原因を一つに絞れる。
package AttrSample; my @ARGS; sub import { ... on_scope_end { while (my $args = shift @ARGS) { # do something by $args } } } sub Stock :ATTR(CODE,BEGIN) { push @ARGS => \@_; }
指定のパッケージでattributeを利用
Attribute::Handlersのドキュメントをみると、指定のパッケージで独自定義したattributeを利用するには継承する例が示されている。しかし、use parent qw(AttrSample Exporter)
のようなコードを書いているとAttrSampleのimportだけが呼ばれ、Exporter側のimportが呼ばれず困るなんてことがある。あった。*2 また、Attribute::Handlersは、sub UNIVERSAL::Stock :ATTR(CODE)
のように、UNIVERSALに生やすパターンも想定しているが、UNIVERSALを使うのは影響範囲が大きいので使っていない。*3
結局、attributeを生やすために必要な最低限の関数を生やせれば良い。
code attributeを定義するには、初心に帰るとMODIFY_CODE_ATTRIBUTESを生やせば良いが、これを独自に書くのは面倒*4。なので、Attribute::Handlersが定義しているMODIFY_CODE_ATTRIBUTESを利用する。具体的には、_gen_handler_AH_
をみると良い。中身をみると、次のことがわかる。
- 行152-173で、ATTR attributeに関する処理をしていて、どのフェーズで実施するかを記録し、
_ATTR_CODE_Stock
といった関数を生やす。 - 行175-196で、
_ATTR_CODE_Stock
といった関数が見つかれば、その実処理をする。
つまり、MODIFY_CODE_ATTRIBUTESと_ATTR_CODE_Stockを指定パッケージに生やせば、Stock attributeは利用できる。 内部実装に依存した書き方になっていることは心配だけれど、Attribute::Handlersなら、そんなに壊れることはないだろうと賭けている。
まとめ
- attributeを用いて、コンパイル時までにチェックに必要な情報を揃えるためには、まずBEGINフェーズで適当な変数にattributeの引数情報を詰め込み、次にimportのon_scope_endで、詰め込んだ引数を元に一気に処理する
- attributeを指定のパッケージで利用できるようにするには、継承ではなく、Attribute::HandlersのMODIFY_CODE_ATTRIBUTESと、Attribute::Handlers用の命名で関数を指定のパッケージに生やす
*1:https://perldoc.perl.org/perlmod#BEGIN,-UNITCHECK,-CHECK,-INIT-and-END
*2:https://github.com/kfly8/p5-Function-Return/pull/22
*3:だがしかし、Attribute::Handlersは、@UNIVERSAL::ISA をいじっているが