Mooseのtriggerの効果的な使い方 ~ lazyとclearerとの併用

| コメント(0) | トラックバック(3)

Mooseのtriggerの効果的な使い方について、遅ればせながら先のMoose入門研修で始めて知りました。簡単なサンプルコードスニペットも用意出来たので、まとめておきます。

この記事の対象読者としては、私と同じMoose初心者の方々を想定しています。

  1. そもそもtriggerとは何か
  2. 具体的な使いどころ
  3. もしtriggerがなければ......
  4. lazyした導出先のアトリビュートをclearすべき
  5. 具体例の数々(サンプルコードスニペット付き)
  6. まとめらしくないまとめ

1. そもそもtriggerとは何か

triggerはMooseのアトリビュートに付加できるオプションの一つです。

Moose::Manual::Attributes(の、石垣さん(charsbarさん)による日本語版)から引用すると、

アトリビュートに値がセットされたときにかならず呼ばれるサブルーチンです。

とあります。

triggerオプションの値はサブルーチンリファレンスを取ります。勿論、匿名サブルーチンリファレンスも設定出来ます。

trigger => \&triggered_method

または

trigger => sub { ... }

などです。

2. 具体的な使いどころ

2.1. ゲッターでは何もせずにセッターの時だけ何かしたい

例えばafterなどのメソッドモディファイヤーの用途として例示されている

  • ログ取得
  • デバッグ

などの稼働範囲をさらに限定する使い方があります。ゲッターの時は何もせずにセッターの時だけ処理したい場合などに使います。

$thing->foo();としてthingfooアトリビュートの値を取得する場合には何もせずに、$thing->foo(1);とした場合にwarn "foo attribute is changed";と表示するなどといった場合です。

2.2. アトリビュート間に関数従属の関係がある

しかし、さらによくある例としては、或るfooアトリビュートが他のbarアトリビュートの計算元となる場合でしょう。barアトリビュートがfooアトリビュートによって導出されるということです。語弊を恐れずにRDB的な用語を使うと、barアトリビュートがfooアトリビュートに関数従属する場合であるとも表現できます。

Moose入門研修のスライド拙訳)には、password(平文パスワード)とhashed_password(ハッシュ化パスワード)という好例が紹介されています。

なお、ハッシュ化パスワードについて補足しておきます。アプリケーションによっては、保安上の観点から、平文パスワードをDBに生で保存せずに、平文パスワードのハッシュ値を保存する設計にすることがあります(認証時には、ユーザーが入力した平文パスワードをハッシュ化し、DBから取り出したハッシュ値と突き合わせます)。ハッシュ化・ハッシュ値についてご存じない方は、暗号化のようなものだと思ってください。

閑話休題。ここで、hashed_passwordpasswordをハッシュ関数に入れて出て来た値なので、これら二つの値は足並みが揃ったものでなくてはなりません。つまり、passwordが変わったならば、それに対応してhashed_passwordも変わらなければ(passwordを使って再計算しなければ)なりません。

クラスを使う側がそんな作業を行うのは馬鹿げています。アプリケーションでpasswordを変える場所毎にhashed_passwordに値を設定するのは大変ですし、漏れが出るに決まっています。さらに、hashed_passwordに入れる値をpasswordから計算するのは、passwordアトリビュートを持つクラス側がやるべき仕事のはずです。

triggerを使えば、passwordが変更されたタイミングにフックして、hashed_passwordを再計算することを、APIの内側で完結した処理として実装することが出来ます。

3. もしtriggerがなければ......

もしtriggerがないと、例えば下記のような見苦しいコードを都度書かなければなりません。

# ...

has 'foo' => (
    is          => 'rw',
    isa         => 'Any',
);

around foo => sub {
    my ($next, $self, @args) = @_;

    # 引数がなければゲッター
    return $self->$next
        unless scalar @args;

    # セッター
    my $return_value = $self->$next(@args);

    # ここにやりたい処理を書く

    return $return_value;
};

# ...

或いは、以下のように。

# ...

has 'foo' => (
    is          => 'rw',
    isa         => 'Any',
    writer      => 'set_foo',
);

after set_foo => sub {
    my $self = shift;

    # ここにやりたい処理を書く

    return;
};

# ...

これでは宣言的なコーディングが出来ませんよね。「セッターの時にこれをしたい」というプログラマーの意図を端的に表現するには、triggerで言明する方が遙かに分かりやすいです。

追記 : うっかりぽん。afterメソッドモディファイヤーでは、引数を「変える」ことは出来ませんが、「読む」ことは出来ました(ここでは、存否を見ています)。従って、少しだけ簡単に、以下のように書けます......といっても、やはりtriggerを使った方が楽ですね。

# ...

    has 'foo' => (
        is          => 'rw',
        isa         => 'Any',
    );

# ...

    after foo => sub {
        my ($self, @args) = @_;

        # 引数がなければゲッター
        return
            unless scalar @args;

        # セッターなのでここにやりたい処理を書く
    };

# ...

4. lazyした導出先のアトリビュートをclearすべき

この記事の一番大事な点はここです。

ここまでtriggerによってpasswordを使ってhashed_passwordを再計算するなどと書いてきましたが、実はこれはあまり良くない例です。Moose入門研修の内容の通り、lazyclearerを使うべきでしょう。

理由はlazyと同様で、導出先のアトリビュートでは値を常時抱えている必要がないからです。プログラムの終了まで、もしくは情報源となるアトリビュートに値が再設定されるまで、導出先のアトリビュートはアクセスされないかも知れません。従って、次回アクセスされてから導出しても(遅延設定しても)問題ないのです。

lazyclearerの組み合わせによって、以下のような素直な実装を簡単に実現出来ます。

package Things;

use Moose;

has 'number' => (   # 数
    is          => 'rw',
    isa         => 'Num',
    trigger     => sub {
        $_[0]->clear_inverse;
    },
);

has 'inverse' => (  # 逆数
    is          => 'ro',
    isa         => 'Num',
    init_arg    => undef,
    lazy_build => 1,
);

sub _build_inverse {
    1 / $_[0]->number;
}

# ...

挙動は以下の通りです。

  1. $things->number(42);によって(またはコンストラクター引数でThings->new(number => 42)などとして)導出元アトリビュートであるnumberが設定される
  2. numberアトリビュートのtriggerで、$things->clear_inverseが呼ばれる
  3. inverseアトリビュートが未設定の状態になる
  4. $things->inverseを呼ぶ
  5. $things->_build_inverseというビルダーメソッドで逆数を(再)設定する

もし4.が呼ばれなければ、5.の逆数の計算もしないという仕組みです。再設定用として別に編集メソッドも用意する必要もありません(すべてbuilderメソッドが面倒を見てくれます)。

後述しますが、相互に導出し合っているようなアトリビュート群があった場合、さらにひどいことになります。是非clearerを使いましょう

5. 具体例の数々(サンプルコードスニペット付き)

延々と書き連ねてきましたが、コードを見た方が早いので、いくつか具体例をご紹介しましょう。

5.1. 一方向の単純導出の場合

まずはこれまで使っていたpasswordhashed_passwordの例です。アトリビュートの関係性をアスキーアートで図示すると、以下のようになります。

password ----> hashed_password

これは上記4.の実装方法と全く同じです。

# ...

use Digest;

# ...

has 'password' => (
    is          => 'rw',
    isa         => 'Str',
    trigger     => sub {
        $_[0]->clear_hashed_password;
    },
);

has 'hashed_password' => (
    is          => 'ro',
    isa         => 'Str',
    init_arg    => undef,
    lazy_build  => 1,
);

sub _build_hashed_password {
    my $digest = Digest->new('SHA-256');
    $digest->add($_[0]->password);

    return $digest->hexdigest;
}

# ...

テストの形をしたサンプルコードは、gist #211069として上げておきました。

今回の説明とは関係ないので上記では書きませんでしたが、いくつか余談を書いておきます。

triggerclearerを指定する際の注意点

passwordで、直接trigger => \&clear_hashed_passwordと書きたいかも知れませんが、これは出来ません。has 'password'した時点ではhashed_passwordは宣言されていないので、つまりhashed_passwordlazy_buildによってMooseが生成してくれるclear_hashed_passwordメソッドも存在しないからです(と理解していましたが、エラーではなく警告が出るだけなので、エラーメッセージ通りに解釈するのが正しそうです)。試しに書いてみると、Moose 0.92では以下のような警告が出ます。

You are overwriting a locally defined method (clear_hashed_password) with an accessor

従って、素直に匿名サブルーチンリファレンスを渡すべきです。

勿論、hashed_passwordpasswordhasの書き順を逆転させればtrigger => \& clear_hashed_passwordと書くことは出来ますが、

  • 導出先アトリビュートが導出元アトリビュートより先に書いてあるとコードの据わりが悪い
  • hasの書き順次第で挙動が変わるのは、Mooseに詳しい人でないと分かりにくい(別の人がコードを修正したときに動かなくなる可能性がある)

というような理由により、無精なことはやめておいた方が得策だと思います。後者などは、例えばロールでrequiresしたメソッドがhasで定義するアトリビュート名(アクセッサー)だった場合に、クラス側でhasの後にwithを書かないと駄目だという問題に似ていて、なかなか厄介な問題です。

hashed_passwordの防衛

hashed_passwordでは

  • is => 'ro'して読み取り専用にする
  • init_arg => undefしてコンストラクターでも設定出来ないようにする

ということをしています。

そもそも、hashed_passwordからpasswordを導出することは出来ません。そもそもハッシュ化とは一方向の変換を前提としているようなものなのですから。従って、hashed_passwordはクラス内部でのみ設定されるようにしておくことが良いでしょう。

より安全にするには、hashed_passwordclearer => '_clear_hashed_password'などとして、クリア用のメソッドをプライベート然とした名前にすることです。

どこまで制約を掛ける?

hashed_passwordMyApp::Typesなどで規定したStrAsHexSHA256というような型にするのは考えどころです。(場合によっては)過ぎたるは及ばざるがごとし、になるかも知れません。というのも、上記のようにhashed_passwordの値の設定はAPI内部で完結しているので、API外部から変な値が入ってくることがないからです(カプセル化の原則が守られている限りに於いては、つまり$things->{hashed_password} = 'foo'などとされない限りは)。

誤りを出来るだけ早期に検知するよう、防衛的なプログラミングをするには、制約を是非掛けておきたいところです。値の計算結果が常に正しいことを、アサーションのように書いておくという意味合いです。

しかし値が何度も再設定されうる場合には、Shawn M Mooreさん(sartakさん)が研修で仰っていたことの受け売りですが、パフォーマンス面での問題が出ないかを気にしておいた方が良いでしょう。

どこまでPBPに準拠する?

PBP("Perl Best Practices", 邦訳は『Perlベストプラクティス』)に準拠していない記述がいくつかあります。

まず、パフォーマンスを気にして、triggerbuilderでは$_[0]という記述を使っています。アプリケーションロジックであれば$self = shift;するのが良いでしょう。また、何も返さないことを明示するreturn;を書いても良いでしょう。しかし、

  • そのサブルーチンやメソッドが極めて単純な宣言的な記述である
  • 今後それが成長することはほとんど考えられない

というようなオレオレ基準に合致すると考えたため、記述を意図的に端折っています。

trigger => sub {
    $_[0]->clear_hashed_password;
},

という記述と、

trigger => sub {
    my $self = shift;

    $self->clear_hashed_password;

    return;
},

という記述のどちらを選ぶかは、基本的には好みの問題です。

業務では「好み」を忖度出来ませんので、コーディング標準として「1センテンスで記述可能なclearerbuilderでは、return;を明示せず、my $self = shift;としない」などと決めてしまう手もあります。

ファットカンマの左辺はクォート不要だよね

その通りです。

ですが、派生クラスで継承したアトリビュートでhas '+foo' => ...などとするので、面倒から基底クラス側でもhas 'foo' => ...してしまえという個人的な好みで書いています。

一方、メソッドモディファイヤーによって修飾される対象のメソッド名はクォートしていません。アトリビュートは「物」のようなものと理解してクォートして名前を付ける一方、メソッドは「振る舞い」なので書き下すような感触なので、こちらは付けていません。PBPでも言及されているような慣習的な命名規則によれば、メソッド名にqr{[a-z\d]}i以外の文字を使うことが殆ど全くない上に、アトリビュートでクォートした最大の理由である継承による+付与がないので、こちらは無精しています。

まあ、全部付けるなら付けるで、しゃっきりした方がいいとは思います。

5.2. 複数の導出元がある場合

次に、name(名前)とtitle(敬称)から、メールなどのaddressee(名宛て)を導出する例で考えてみましょう。アトリビュートの関係性をアスキーアートで図示すると、以下のようになります。

name  --+
        +-> addressee
title --+

これは前項の応用編です。addresseebuildernametitleを使うので、nametitleそれぞれでclear_addresseeを呼べば良いです。

# ...

has 'name' => (
    is          => 'rw',
    isa         => 'Str',
    trigger     => sub {
        $_[0]->clear_addressee;
    },
);

has 'title' => (
    is          => 'rw',
    isa         => 'Str',
    trigger     => sub {
        $_[0]->clear_addressee;
    },
);

has 'addressee' => (
    is          => 'ro',
    isa         => 'Str',
    lazy_build  => 1,
);

sub _build_addressee {
    $_[0]->title . q{ } . $_[0]->name;
}

# ...

これもgist #211070に保存しています。

_build_addresseeの書きぶり

addresseeが多くのアトリビュートに依拠するならば、_build_addresseeメソッドではsprintfを使った方がコードが綺麗になります。

また、もしtitleis => Maybe[Str](つまりis => 'Undef | Str')だった場合には、

defined $_[0]->title ? $_[0]->title . q{ } . $_[0]->name
                     :                       $_[0]->name;

となるでしょう。

各アトリビュートをより堅くする

例えば業務アプリケーションなどでは以下のようなことをすると思います。

  • namefirst_name(またはforename)とlast_name(またはfamily_nameやらsurnameやら)に分けるべき
  • それらの名前の文字長はDBスキーマのvarchar(XXX)などと足並みを揃えた制約を施す(subtypeを使う)と堅い
  • titleenum(Mr. Ms. Mrs. Miss Dr.)などの選択式にする方が堅い

5.3. 循環的に導出し合っている場合

例えばimperial_era(和暦)とchristian_era(西暦)の関係がこれに当たります。アスキーアートによるアトリビュートの関係性は以下の通りです。

imperial_era <---> christian_era

これは双方のアトリビュートが共に「導出元であり導出先である」と整理出来ますので、以下のように書けます。

package Syouwa;

# ...

our $Delta = 1925;

has 'syouwa_era' => (       # 昭和X年
    is          => 'rw',
    isa         => 'Int',
    lazy_build  => 1,
    trigger     => sub {
        $_[0]->clear_christian_era;
    },
);

has 'christian_era' => (    # 西暦X年
    is          => 'rw',
    isa         => 'Int',
    lazy_build  => 1,
    trigger     => sub {
        $_[0]->clear_syouwa_era;
    },
);

sub build_syouwa_era {
    return $_[0]->syouwa_era + $Delta;
}

# ...

gist #211073には、下記の余談にあるロール版とサブクラス版それぞれを例示するコードを保存しました。

続・triggerclearerを指定する際の注意点

syouwa_eratrigger => \&clear_christian_eraなどと書きたくても駄目なことは、5.1節でご紹介しました。後に書いた西暦の方では(既にclear_showa_eraが存在するので)trigger => \&clear_showa_eraすること自体は可能ですが、記述を統一しておいた方が分かりやすいでしょう。また、そうしておくと、後述のように、トリガーで複数の処理を行いたい場合にも、当該匿名サブルーチン内に処理を書き加えるだけで済みます。

年の制約

より正しくは昭和にwhere { 1 <= $_ && $_ <= 64}という制約を施すべきです。 また、西暦もwhere { 1 <= $_ }すべきでしょう。紀元前は考慮していません。

各アトリビュートのリファレンスを持つ場合

リファレンスによって同一オブジェクト内のアトリビュート間での依存性を表現する場合でも、(異なるオブジェクトのアトリビュート間での循環参照時と同様に)weak_ref => 1することを忘れないようにしてください。

サブクラス化するかロール化する

上記では昭和クラスを想定していましたが、これはいまいちです。なぜなら、明治・大正・平成などに流用が効かないクラスになっているからです。

  • 西暦を持つロールと、各元号クラスに分ける
  • 元号抽象クラスと、昭和具象クラスに分ける

などの方法で、固定部分と可変部分を峻別しましょう。

ついでに、クラス変数として持っているour $Deltaも、MooseX::ClassAttributeを使ったりロール側で定義するアトリビュートにしたり(その場合はbuild_deltarequiresすることになります)と、手を入れておくことも出来ます。

他の例

西暦と皇紀でも大体似たようなものですが、実際には旧暦などを考えなければいけないので気を付けてください。

拙作MooseX::Types::Locale::Language(ISO 639-1の言語コードの制約と型変換)でも、これと同じ状況の例をサンプルコードMooseX-Types-Locale-Language-0.003/examples/complex.plとして付属しています。なお、このモジュールについては別の機会に記事を書くつもりです。

もしclearerを使わないなら......

もしclearerを使わずに関係先のアトリビュートを直接その場で設定しようとするなら、以下のような厄介なことを解決しなければなりません。

  • 無限ループを防ぐ仕組みを仕込まなければなりません
  • builderと同じ処理を書かなければなりません

無限ループについてですが、これは以下のようにして発生します。

  1. $era->imperial_era(1);で元号を設定する
  2. 元号のtriggerによって、$self->christian_era( $self->imperial_era + $Delta );などとして西暦を設定する
  3. 西暦のtriggerによって、$self->imperial_era( $self->christian_era - $Delta );などとして元号を設定する
  4. 2.に戻る

これを防ぐ方法は、例えば以下の通りです。

  • セッターメソッドをアトリビュート名とは別の名称にする
  • トリガーには匿名でないサブルーチンリファレンスを設定する
  • 呼び元のメソッド名を見て、ループを断ち切る

具体的な動きとしては、以下のようになります。

  1. $era->_set_imperial_era(1);で元号を設定する
  2. 元号のtrigger$self->_build_by_imperial_eraを呼ぶ
  3. 2つ前のメソッドが_set_christian_eraではないので、$self->_set_christian_era( $self->imperial_era + $Delta );として西暦を設定する
  4. 西暦のtrigger$self->_build_by_christian_eraを呼ぶ
  5. 2つ前のメソッドが_set_imperial_eraなので、$self->_set_imperial_eraは呼ばずにループを断ち切る

メソッド名を得る実装としては、(caller(2) )[3]で得られる完全修飾メソッド名を使うことになります。

......ああっ、なんて面倒くさいのでしょうか! とてもやっていられませんが、実はMoose入門研修受講前の私は正にそれをやっていました。私の失敗例もgist #211088として晒しておきましょう。これはひどい

5.4. さらなる応用例

上記5.3の最後にMooseX::Types::Locale::Languageでの例を掲げました。これは

alpha2 <---> name

という関係性でしたが、MooseX::Types::Locale::Country(ISO 3166-1の国コードの制約と型変換)ではさらに複雑になっています。

   +------> alpha2 <------+
   |           ^          |
   v           |          v
numeric <------+------> name
   ^           |          ^
   |           v          |
   +------> alpha3 <------+

何かの悪い冗談のようですが、これも結局は関係先全てのclearerを呼んで、builderでは関係先のどれかの値を使うようにすればよいのです。

サンプルコードMooseX-Types-Locale-Country-0.000/examples/complex.plの中身を少し転載しておきます。

# ...

    use MooseX::Types::Locale::Country qw(
        Alpha2Country
        Alpha3Country
        NumericCountry
        CountryName
    );
    use Locale::Country;

# ...

    has 'alpha3' => (
        is          => 'rw',
        isa         => Alpha3Country,
        init_arg    => '_alpha3',
        coerce      => 1,
        lazy_build  => 1,
        writer      => '_set_alpha3',
        trigger     => sub {
            $_[0]->clear_alpha2;
            $_[0]->clear_numeric;
            $_[0]->clear_name;
        },
    );

# ...

    sub _build_alpha3 {
          $_[0]->has_alpha2
            ? country_code2code
                ( $_[0]->alpha2,  LOCALE_CODE_ALPHA_2, LOCALE_CODE_ALPHA_3 )
        : $_[0]->has_numeric
            ? country_code2code
                ( $_[0]->numeric, LOCALE_CODE_NUMERIC, LOCALE_CODE_ALPHA_3 )
        :
              country2code
                ( $_[0]->name,    LOCALE_CODE_ALPHA_3 );
    }

# ...

predicateオプション(省略時はlazy_buildオプションの有効時にはhas_ATTRIBUTE)を使っているのが着目点です。

6. まとめらしくないまとめ

triggerclearerlazyを組み合わせて使う、それが要点です。

上記5.3.では、triggerclearerlazyの組み合わせを知らなかった時期の醜悪なコードを恥を忍んで晒しました。私のようなMoose初心者の方には、これを反面教師としていただけるかも知れません。

Mooseは大変強力です。Mooseを使ってプログラミングする時、「これって気持ち悪い書き方だよね」と思ったら、Moose::ManualMoose::Cookbookを漁りましょう。きっと洗練された素直な書き方が出来るはずです。

無駄なことを頑張って仕上げるようなガチムチな仕事は避けましょう。大いなる自戒を込めて......。

追記 : 三宅さん(nekoyaさん)のはてブコメントで、triggerは一つしか書けないけど、afterは追加できるという違いは大きいと思うという指摘をいただきました。単に導出先の値をリセットしたいだけならばclearerを使えば済みますが、ロールなどでtriggerされるメソッドの挙動をいじりたい場合には、メソッドモディファイヤーも有用ですね。なお、メソッドモディファイヤーをclearerのメソッドに適用するという合わせ技も出来ますので、夢が拡がります。

トラックバック(3)

Mooseのアトリビュートには、多くのオプションがあります。これらはハッシュとし... 続きを読む

MVCのModelをWAFから切り離しつつ、Modelも(DBとの仲立ちをさせる... 続きを読む

Mooseクラスの生成時に、コンストラクター引数として与えられたハッシュまたはハ... 続きを読む

コメントする

筆者"Gardejo"について

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

このサイトについて

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

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

関連サイト

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

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

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

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

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

このブログ記事について

このページは、Gardejoが2009年10月16日 02:33に書いたブログ記事です。

ひとつ前のブログ記事は「Moose入門研修の演習問題を訳出 ~ gitの使い方の反省」です。

次のブログ記事は「Mooseアトリビュートのオプション指定順についての私案」です。

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

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  

やや真面目なサイト