問題
Attribute::*系のモジュール (Attribute::Protected, Attribute::Final, Attribute::Alias, Attribute::Constant, Attribute::Method, etc…)を使ったモジュールを動的にロードしたい。
解法
importメソッドかBEGINブロックでrequire, importします。詳細は以下の通りです。
Called.pm
package Called;
use 5.010_000;
use strict;
use warnings;
use Attribute::Util qw(Abstract Alias Constant Final Protected);
sub foo : Public {
say "foo";
return;
}
sub bar : Protected {
say "bar";
return;
}
# ...
1;
Caller.pm
package Caller;
use 5.010_000;
use strict;
use warnings;
use Attribute::Util qw(Abstract Alias Constant Final Protected);
use Carp;
use English;
my $target_class_name; # package global
sub import : Public {
$target_class_name = _get_target_class_name();
eval "require $target_class_name";
if ($EVAL_ERROR) {
croak 'xxx';
}
$target_class_name->import();
# ...
return;
}
sub dispatch : Public {
# *** Compile-time error !! ***
# my $target_class_name = _get_target_class_name();
# eval "require $target_class_name";
# if ($EVAL_ERROR) {
# croak 'xxx';
# }
# $target_class_name->import();
my $target_method = _get_target_method();
$target_class_name->$target_method();
return;
}
sub _get_target_class_name : Private {
return 'Called'; # 例。本来は動的に取得。
}
sub _get_target_method : Private {
return 'foo'; # 例。本来は動的に取得。
}
# ...
1;
caller.cgi
use 5.010_000;
use strict;
use warnings;
use Caller;
Caller->dispatch();
exit;
実行結果
foo
解説
問題の検証
モジュールの動的ロードの一般解
モジュールは素直にuse SameModuleNameするだけではなく、動的にロードすることも出来ます。
通常、モジュールの動的ロードには二つの方法があります(importは以下の例では省略しています)。
require $SameModulePath;eval "require $SameModuleName";
perldoc -f useやperldoc -f requireの復習ですが、use $Variableすることは出来ませんので、requireして後で$SameModuleName->importか$SameModuleName::importする必要があります。また、require $SameModuleNameすることも出来ませんので、evalブロックで括ってあげる必要があります。
Attributeの効用
Attributeとは何ぞやという話は、日本語の情報だと大沢和宏(Yappo)さんによるYappoLogsの「PerlのAttributeについてのお勉強」などの一連の記事をご覧いただければ良いかと思います。サブルーチンや変数に属性(attribute)を追加できる仕組みなのですが、その仕組みをどう実装するかは自由です。
エスペラント日本語翻訳システムErmitejoでは自分でAttributeをこさえたりするのではなく、Abstract, Alias, Constant, Final, Method, Private, Protected, Publicなどの属性を定義しているCPANモジュールを使わせていただいています(まとめてAttribute::Utilでロードすると楽)。Ermitejoくらいの規模のシステムになると、Perlの自由な気風が自分の首を絞めることもあるので、自律のためにJava然としたアクセス修飾子を導入しています。
ただ、仕組み上どうしても1割方遅くなるために運用コードでは外す選択肢もあります。その場合はコードからAttributeを取り除くのではなく、オレオレライブラリに空実装した同名モジュールを置くのが楽でしょう。そもそもAttribute::Finalだけ属性がfinalのように小文字始まりなのが我慢ならない人(あ、私ですね)はオレオレAttribute::Finalを作るので、それと似たようなことではありますが。なお、その場合でもAttribute::Methodは生かしておかないと、use strict環境下(より具体的にはuse strict "vars"環境下)では動きません。余談ついでになりますが、Attribute::Protectedの作者である宮川達彦さんも大文字小文字では悩んでいらしたようです(PODの記述による)。
Attributeと動的ロードは相性が悪い
さて、そのように便利なAttributeですが、私の使っている各種Attributeは、動的ロードとは相性が悪いようです。そもそもモジュールは好きな場所(スコープ)で動的にロードすることが出来ますが、呼びたいモジュール(のサブルーチン, メソッド)がAttributeを使っていると、以下のようなエラーが出ます。
Unknown error
Compilation failed in require at /usr/local/t/caller.cgi line 7.
BEGIN failed--compilation aborted at /usr/local/t/caller.cgi line 7.
Can't use string ("ANON") as a symbol ref while "strict refs" in use at /usr/local/lib/perl5/site_perl/5.10.0/Attribute/Protected.pm line 26.
CHECK failed--call queue aborted at /usr/local/t/caller.cgi line 7.
私はAttribute周りをブラックボックスとして使っているので、処理を追ってどこで何が突っ掛かっているのかをつまびらかにすることはまだ出来ません。ただ、Caller.pmの各サブルーチン, メソッドへのAttribute適用との順序性でこけているような感触を得ました。これは、
- Caller.pm側のAttributeを外すとエラーが起きない
- Called.pm側のAttributeを外すとエラーが起きない
などの事象が傍証となっているような気がします。
use strict環境下(より具体的にはuse strict "vars"環境下)で死ぬ実装は今時あり得ないですし、evalをさらにブロックで囲ってno strict "vars"するインチキも無効です。
期待するゴール
Caller.pmのuse Attribute::*より先にCallerd.pmをロードすることで、本件の問題は回避出来ます。
「この際どのスコープでもいいから使いたい」というのであれば、BEGINに書いてしまえばいいです。ただし、それではCalled.pmを呼ぶ際のimportへの引数など、Caller.pm側の処理をcaller.cgiで定義したい場合に不都合が生じます。
動的ローディングの実装
ということで、結論としてはimportに実装しましょう。こればかりは書く先(スコープ)の問題なので、「解法」以上の話はありません。
敢えて蛇足を加えるならば、importではディスパッチしたくない場合(dispatchメソッドでディスパッチしたい場合)には、ロードするクラス名を(Caller.pmの)クラス変数として持っておくのが手っ取り早いです。
Attribute周りの内部は今は詳しくないので、より洗練された方法などのツッコミがありましたら教えてください。
備考
エスペラント日本語翻訳システムErmitejoへの適用を視野にフレームワークを学ぶつもりでしたが、今をときめくCatalystなどに実際に移植しようとした結果、色々と思うところがあって、結局オレオレフレームワークになってしまいました。その辺りの紆余曲折はまた別の機会にでもご紹介するかも知れません。
ともあれ、オレオレフレームワークといえどMVC構造は備えています。Controllerクラスにdispatchする辺りは、呼び元のCGIスクリプトにつらつらとuseを連ねるのではなく、「業務制御表」上に定義したクラスをロードし、かつ同表に定義したメソッドを使うようにしています。
業務制御表には他にも
- 業務毎の稼働・停止の区分を定義し、システム全体の開放・閉鎖とは別に、個別業務の流入許可・抑止に使う
- 業務実行権限を定義し、ユーザ側の権限レベルと突き合わせて実行諾否を制御する
- 応答雛形識別子(結果画面の
Template-Toolkit用雛形ファイル)を定義し、Viewクラスで使う - 実行時には「予めPerlのデータ構造に読み替えて
Storableでnstoreしたもの」をretrieveしている - 呼び元のURIを環境変数から読んで業務制御表のキーとして使う
等々の話題がありますが、流石に余談が過ぎるので紹介の機会を改めることにします。
閑話休題。上記のように動的にロードしたかったのですが、意外と単純なところで落とし穴にはまっていました。実際にimportメソッドかBEGINブロックに書けばいいという解法に辿り着くまで、風邪気味の頭を振り回す羽目になりました。
例によって、上記は開発中のバージョンでの実装内容を抽出して例示した物であり、Ermitejo本体とは異なります。また、2008年12月現在で公開している辞書引き機能はオレオレフレームワークを使っていません。その辺りの適用は、やはり正月明けになりそうです。オレオレフレームワークの頭字語を考えて年の瀬を迎えた、という冗談が真にならないように、まずは風邪を治すこととします。