Mooseクラスでのクラス定数は、MooseX::ClassAttributeで定義する方法もありますが、素直なのはクラスメソッドとして実装してしまうことでしょう。これならば、ロールとの相性も良いです。
しかし、クラス定数(のように使うクラスメソッドの戻り値)が単純な値であればともかく、何かの値を元に計算して導出するような代物だった場合、クラス定数(のように......以下略)を使う度に計算されたのではたまりません。
こうした場合には、Memoizeによるメモ化(memoization)が定番の処方箋となります。また、(もはやメモ化の文脈での例としては定番である)フィボナッチ数の計算など、クラス定数として使うものでないメソッドについても、同様にメモ化は強力な最適化方法論です。
Mooseクラスでは単純にMemoizeモジュールを使えば良いのですが、Mooseロールでは少々込み入った手順で使う必要があることが分かったので、備忘録的に書いておきます。
Mooseクラスにあるメソッドのメモ化
まず、Mooseクラスの例です。素直にmemoize関数を使えます。
{
package Foo;
use Moose;
use Memoize;
use namespace::clean -except => [qw(meta)];
memoize qw(bar);
sub bar {
warn "bar\n";
return;
}
__PACKAGE__->meta->make_immutable;
}
{
package main;
my $foo = Foo->new;
$foo->bar;
$foo->bar;
}
Attribute::MemoizeによるMemoizeアトリビュートを使うことも可能です。なお、大変こんがらがり易いのですが、MemoizeアトリビュートというのはMooseのアトリビュートではなくて、Perlの(サブルーチンや変数に付与する)アトリビュートです。
# ...
use Moose;
use Attribute::Memoize; # またはuse Attribute::Util qw(Memoize);
use namespace::clean -except => [qw(meta)];
sub bar : Memoize {
warn "bar\n";
return;
}
# ...
Mooseロールにあるメソッドのメモ化
ロール消費クラスからメソッドが見えない
ロールでは単純には行きません。以下はCan't locate object method "bar" via package "Foo"という例外を送出します。もう少し正確に定義すると、Foo->meta->has_method('bar')は偽になります。ロール内でメモ化したメソッドは、ロールを消費するクラスからは見えない状態にあるのです。
{
package FooBase;
use Moose::Role;
use Memoize;
use namespace::clean;
memoize qw(bar);
sub bar {
warn "bar\n";
return;
}
1;
}
{
package Foo;
use Moose;
use namespace::clean -except => [qw(meta)];
with qw(
FooBase
);
__PACKAGE__->meta->make_immutable;
}
{
package main;
my $foo = Foo->new;
$foo->bar;
$foo->bar;
}
memoizeにサブルーチンリファレンスを設定する
上記のように嵌ったのでググってみたところ、moose@perl.orgメーリングリストでMemoizing role methodsという、そのものずばりの質疑応答がありました。
有名なMSTことMatt S Troutさんによると、
Memoizeのマニュアルには、memoize関数にはメソッド名だけでなくサブルーチンリファレンスも渡せると書いてあるmemoize関数の戻り値として、メモ化でラッピングしたサブルーチンリファレンスが返るとも書いてある__PACKAGE__->meta->add_method('foo_method' => memoize(sub { ... }));とすれば、メモ化ラッピングサブルーチンに名前を付けて、消費元のクラス(consumer class)にメソッドを生やせる- もしくは
memoize('foo_method')の戻り値であるサブルーチンリファレンスをSub::Nameのsubnameに食わせることで、元のメソッド名を乗っ取って、メモ化ラッピングサブルーチン自体が呼ばれるようにすることが出来る
ということです。
subnameの戻り値をさらにグロブリファレンスに入れる理由は、Sub::NameのPODに書いてあります。おまじないだと思って書くことにしましょう。
現在の私の実装
上記を踏まえて、私は現在のところ以下のように実装しています。別の話題(別途記事を設ける予定の「実行時まで遅延させてMooseの型を生成・適用する方法」)の実現可能性調査で使ったGist: #230058から例示します。
# ...
sub _memoize {
no strict 'refs';
foreach my $method (qw(
_numbers _names
_names_to_numbers _numbers_to_names
to_number to_name
maximum_number maximum_name
minimum_number minimum_name
)) {
*{$method} = subname __PACKAGE__ . '::' . $method => memoize($method);
}
}
__PACKAGE__->_memoize;
# ...
メモ化したい対象をforeachのリストに突っ込むだけで、後はいい感じに動いてくれます。
MooseロールにあるメソッドにMemoizeアトリビュートを付与する
これだけでも基本的には満足出来るのですが、メモ化するメソッドの一覧を書くことは、少々面倒です。
希望としては、メソッド側にMemoizeアトリビュートを付与しつつ、各メソッドを列挙することなしに名前空間のマジックに連携させることです。
メソッドにMemoizeアトリビュートを付与すること自体は可能
まず、メソッド側にMemoizeアトリビュートを付与すること自体が出来る、という確認です。
# ...
sub _memoize {
no strict 'refs';
foreach my $method_name (qw(
_numbers _names
_names_to_numbers _numbers_to_names
to_number to_name
maximum_number maximum_name
minimum_number minimum_name
)) {
*{$method} = subname __PACKAGE__ . '::' . $method => \&{$method};
}
}
sub _numbers : Memoize {
# ...
}
__PACKAGE__->_memoize;
# ...
これで問題なく想定の挙動をしてくれます。しかし、これでは何の解決にもなっていませんね。かえって、各メソッドと_memoizeメソッドの両方でメモ化対象を指定することになるだけ、多重保守の愚を犯してしまいます。DRYでないということです。
メソッドにMemoizeアトリビュートが付与されているかを調査する
メソッド側でMemoizeアトリビュートを付与しさえすれば、後は何もしなくて良い......ということが理想です。そのためには、_memoizeメソッドに於いて全メソッドを見て回って、各々にMemoizeアトリビュートが付いているかどうかを確認し、付いていればsubnameの流れに乗る、ということが出来れば良いわけです。
# ...
use attributes qw(); # 流石にgetという名前はインポートしたくない
use List::MoreUtils qw(any); # でもanyはいいんです(オレオレ二枚舌基準)
# ...
sub _memoize {
my $clss = shift;
foreach my $method ($class->meta->get_all_methods) {
no strict 'refs';
*{ $method->name } = subname $class . '::' . $method->name
=> $method->body
if any { $_ eq 'Memoize' } attributes::get($method->body);
}
}
# ...
ところが、上記の実装は動きません。ロールにはmetaメソッドがない(ロールはMoose::Objectを持っていない)ためです。
それでは、ロールを消費するクラスのメタオブジェクトを見ようとすると、$consumer_class->metaとなるようにロールを消費するクラスの側で__PACKAGE__->_memoizeする方法などが考えられます。
しかしその案も駄目です。クラス側で_memoizeを呼んだ時には、既にロールはコンパイルされていますので、ロールでMemoizeアトリビュートを付けたメソッドはforeachのリストには入ってきません。
なお、ここまでしれっと書いていますが、そもそもattributes::getは組み込みアトリビュート(locked, methodなど)以外のアトリビュートが帰ってこないようです。従って、実はメソッドがMemoizeアトリビュート付きかどうかを判別する方法が、本日現在の私には分からないというお粗末な事情があります。
サブルーチン定義時に何とかMemoizeする
後からメソッドを洗うことを諦め、メソッドを定義した時点でメモ化しつつsubnameの仕組みに乗っかる案もありました。
# ...
*foo = subname __PACKAGE__ . '::foo' => sub : Memoize {
# foo本来の実装
};
*bar = subname __PACKAGE__ . '::bar' => sub : Memoize {
# bar本来の実装
};
# ...
しかし、無名サブルーチンへのアトリビュートの適用は出来ない(と理解している)ため、これも動きません。
ここまで来れば別にAttribute::Memoizeを使わずとも良いわけで、以下のような形でなら定義出来ます。周り巡って出発地点に戻ったような、少々異様なコードになりました。
# ...
{
no warnings 'once';
*foo = subname __PACKAGE__ . '::foo' => memoize(sub {
# foo本来の実装
});
*bar = subname __PACKAGE__ . '::bar' => memoize(sub {
# bar本来の実装
});
}
# ...
だったら素直に以下のようにした方が直感的かも知れません。いずれにせよ微妙ですが......。
# ...
{
no warnings 'once';
*foo = subname __PACKAGE__ . '::foo' => \&foo;
sub foo : Memoize {
# foo本来の実装
}
*bar = subname __PACKAGE__ . '::bar' => \&bar;
sub bar : Memoize {
# bar本来の実装
}
}
# ...
まとめ ~ 敗北宣言
開き直ってロールを消費するクラス側でメモ化してしまう手もありますが、それでもやはりDRYではありません。Perlのアトリビュート周りも、MooseやClass::MOPの奥深い部分も、私は殆ど理解せずにブラックボックス的に使ってしまっているので、もう少し勉強をしてみたいです。
MSTさんも「Memoizeのマニュアルをちゃんと読まない人達のために、Mooseのドキュメントに追記が必要ですか?」と記しているように、もしかしたらご立腹されているのかも知れません。ごめんなさい、ちゃんとドキュメントを読みます。
機会があったら試行錯誤して雪辱を果たそうと思います。
コメントする