Mooseロールに実装したメソッドをmemoizeする方法

| コメント(0)

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さんによると、

  1. Memoizeのマニュアルには、memoize関数にはメソッド名だけでなくサブルーチンリファレンスも渡せると書いてある
  2. memoize関数の戻り値として、メモ化でラッピングしたサブルーチンリファレンスが返るとも書いてある
  3. __PACKAGE__->meta->add_method('foo_method' => memoize(sub { ... }));とすれば、メモ化ラッピングサブルーチンに名前を付けて、消費元のクラス(consumer class)にメソッドを生やせる
  4. もしくはmemoize('foo_method')の戻り値であるサブルーチンリファレンスをSub::Namesubnameに食わせることで、元のメソッド名を乗っ取って、メモ化ラッピングサブルーチン自体が呼ばれるようにすることが出来る

ということです。

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のドキュメントに追記が必要ですか?」と記しているように、もしかしたらご立腹されているのかも知れません。ごめんなさい、ちゃんとドキュメントを読みます。

機会があったら試行錯誤して雪辱を果たそうと思います。

コメントする

筆者"Gardejo"について

  • Twitter: @gardejo
  • GitHub: gardejo
  • CodeRepos: gardejo
  • CPAN: MORIYA

このサイトについて

Eorzea System Worksは、架空のシステム開発結社です。

FF14.name (FinalFantasyXIV.name)では、アヴァター(プレイヤーキャラクター)の管理システムやイベント出欠・リマインダシステムや、リンクシェル(LS)運営・管理システムやDKPシステムなどを設計・開発・公開する予定です。

関連サイト

関連サイトでは、他にもFF14に関連するサイトをいくつか紹介しています。

リンク, トラックバック歓迎

このブログへのリンク(どのページでも構いません)やトラックバックを歓迎します。

設計・開発・運用の参考にさせていただきますので、コメントもお気軽にお寄せください!

個別の記事に対するご意見などのほか、目安箱もご用意しています。

このブログ記事について

このページは、Gardejoが2009年11月11日 01:43に書いたブログ記事です。

ひとつ前のブログ記事は「Mooseクラスのコンストラクター引数でハッシュとハッシュリファレンスを同一視する方法」です。

次のブログ記事は「Perlでtrim(さらに完全版)」です。

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。

2014年2月

            1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28  

やや真面目なサイト