Mooseのtriggerの効果的な使い方について、遅ればせながら先のMoose入門研修で始めて知りました。簡単なサンプルコードスニペットも用意出来たので、まとめておきます。
この記事の対象読者としては、私と同じMoose初心者の方々を想定しています。
- そもそも
triggerとは何か - 具体的な使いどころ
- もし
triggerがなければ...... lazyした導出先のアトリビュートをclearすべき- 具体例の数々(サンプルコードスニペット付き)
- まとめらしくないまとめ
1. そもそもtriggerとは何か
triggerはMooseのアトリビュートに付加できるオプションの一つです。
Moose::Manual::Attributes(の、石垣さん(charsbarさん)による日本語版)から引用すると、
アトリビュートに値がセットされたときにかならず呼ばれるサブルーチンです。
とあります。
triggerオプションの値はサブルーチンリファレンスを取ります。勿論、匿名サブルーチンリファレンスも設定出来ます。
trigger => \&triggered_method
または
trigger => sub { ... }
などです。
2. 具体的な使いどころ
2.1. ゲッターでは何もせずにセッターの時だけ何かしたい
例えばafterなどのメソッドモディファイヤーの用途として例示されている
- ログ取得
- デバッグ
などの稼働範囲をさらに限定する使い方があります。ゲッターの時は何もせずにセッターの時だけ処理したい場合などに使います。
$thing->foo();としてthingのfooアトリビュートの値を取得する場合には何もせずに、$thing->foo(1);とした場合にwarn "foo attribute is changed";と表示するなどといった場合です。
2.2. アトリビュート間に関数従属の関係がある
しかし、さらによくある例としては、或るfooアトリビュートが他のbarアトリビュートの計算元となる場合でしょう。barアトリビュートがfooアトリビュートによって導出されるということです。語弊を恐れずにRDB的な用語を使うと、barアトリビュートがfooアトリビュートに関数従属する場合であるとも表現できます。
Moose入門研修のスライド(拙訳)には、password(平文パスワード)とhashed_password(ハッシュ化パスワード)という好例が紹介されています。
なお、ハッシュ化パスワードについて補足しておきます。アプリケーションによっては、保安上の観点から、平文パスワードをDBに生で保存せずに、平文パスワードのハッシュ値を保存する設計にすることがあります(認証時には、ユーザーが入力した平文パスワードをハッシュ化し、DBから取り出したハッシュ値と突き合わせます)。ハッシュ化・ハッシュ値についてご存じない方は、暗号化のようなものだと思ってください。
閑話休題。ここで、hashed_passwordはpasswordをハッシュ関数に入れて出て来た値なので、これら二つの値は足並みが揃ったものでなくてはなりません。つまり、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入門研修の内容の通り、lazyとclearerを使うべきでしょう。
理由はlazyと同様で、導出先のアトリビュートでは値を常時抱えている必要がないからです。プログラムの終了まで、もしくは情報源となるアトリビュートに値が再設定されるまで、導出先のアトリビュートはアクセスされないかも知れません。従って、次回アクセスされてから導出しても(遅延設定しても)問題ないのです。
lazyとclearerの組み合わせによって、以下のような素直な実装を簡単に実現出来ます。
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;
}
# ...
挙動は以下の通りです。
$things->number(42);によって(またはコンストラクター引数でThings->new(number => 42)などとして)導出元アトリビュートであるnumberが設定されるnumberアトリビュートのtriggerで、$things->clear_inverseが呼ばれるinverseアトリビュートが未設定の状態になる$things->inverseを呼ぶ$things->_build_inverseというビルダーメソッドで逆数を(再)設定する
もし4.が呼ばれなければ、5.の逆数の計算もしないという仕組みです。再設定用として別に編集メソッドも用意する必要もありません(すべてbuilderメソッドが面倒を見てくれます)。
後述しますが、相互に導出し合っているようなアトリビュート群があった場合、さらにひどいことになります。是非clearerを使いましょう。
5. 具体例の数々(サンプルコードスニペット付き)
延々と書き連ねてきましたが、コードを見た方が早いので、いくつか具体例をご紹介しましょう。
5.1. 一方向の単純導出の場合
まずはこれまで使っていたpasswordとhashed_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として上げておきました。
今回の説明とは関係ないので上記では書きませんでしたが、いくつか余談を書いておきます。
triggerでclearerを指定する際の注意点
passwordで、直接trigger => \&clear_hashed_passwordと書きたいかも知れませんが、これは出来ません。has 'password'した時点ではhashed_passwordは宣言されていないので、つまりhashed_passwordのlazy_buildによってMooseが生成してくれるclear_hashed_passwordメソッドも存在しないからです(と理解していましたが、エラーではなく警告が出るだけなので、エラーメッセージ通りに解釈するのが正しそうです)。試しに書いてみると、Moose 0.92では以下のような警告が出ます。
You are overwriting a locally defined method (clear_hashed_password) with an accessor
従って、素直に匿名サブルーチンリファレンスを渡すべきです。
勿論、hashed_passwordとpasswordのhasの書き順を逆転させれば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_passwordでclearer => '_clear_hashed_password'などとして、クリア用のメソッドをプライベート然とした名前にすることです。
どこまで制約を掛ける?
hashed_passwordをMyApp::Typesなどで規定したStrAsHexSHA256というような型にするのは考えどころです。(場合によっては)過ぎたるは及ばざるがごとし、になるかも知れません。というのも、上記のようにhashed_passwordの値の設定はAPI内部で完結しているので、API外部から変な値が入ってくることがないからです(カプセル化の原則が守られている限りに於いては、つまり$things->{hashed_password} = 'foo'などとされない限りは)。
誤りを出来るだけ早期に検知するよう、防衛的なプログラミングをするには、制約を是非掛けておきたいところです。値の計算結果が常に正しいことを、アサーションのように書いておくという意味合いです。
しかし値が何度も再設定されうる場合には、Shawn M Mooreさん(sartakさん)が研修で仰っていたことの受け売りですが、パフォーマンス面での問題が出ないかを気にしておいた方が良いでしょう。
どこまでPBPに準拠する?
PBP("Perl Best Practices", 邦訳は『Perlベストプラクティス』)に準拠していない記述がいくつかあります。
まず、パフォーマンスを気にして、triggerやbuilderでは$_[0]という記述を使っています。アプリケーションロジックであれば$self = shift;するのが良いでしょう。また、何も返さないことを明示するreturn;を書いても良いでしょう。しかし、
- そのサブルーチンやメソッドが極めて単純な宣言的な記述である
- 今後それが成長することはほとんど考えられない
というようなオレオレ基準に合致すると考えたため、記述を意図的に端折っています。
trigger => sub {
$_[0]->clear_hashed_password;
},
という記述と、
trigger => sub {
my $self = shift;
$self->clear_hashed_password;
return;
},
という記述のどちらを選ぶかは、基本的には好みの問題です。
業務では「好み」を忖度出来ませんので、コーディング標準として「1センテンスで記述可能なclearerやbuilderでは、return;を明示せず、my $self = shift;としない」などと決めてしまう手もあります。
ファットカンマの左辺はクォート不要だよね
その通りです。
ですが、派生クラスで継承したアトリビュートでhas '+foo' => ...などとするので、面倒から基底クラス側でもhas 'foo' => ...してしまえという個人的な好みで書いています。
一方、メソッドモディファイヤーによって修飾される対象のメソッド名はクォートしていません。アトリビュートは「物」のようなものと理解してクォートして名前を付ける一方、メソッドは「振る舞い」なので書き下すような感触なので、こちらは付けていません。PBPでも言及されているような慣習的な命名規則によれば、メソッド名にqr{[a-z\d]}i以外の文字を使うことが殆ど全くない上に、アトリビュートでクォートした最大の理由である継承による+付与がないので、こちらは無精しています。
まあ、全部付けるなら付けるで、しゃっきりした方がいいとは思います。
5.2. 複数の導出元がある場合
次に、name(名前)とtitle(敬称)から、メールなどのaddressee(名宛て)を導出する例で考えてみましょう。アトリビュートの関係性をアスキーアートで図示すると、以下のようになります。
name --+
+-> addressee
title --+
これは前項の応用編です。addresseeのbuilderはnameとtitleを使うので、nameとtitleそれぞれで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を使った方がコードが綺麗になります。
また、もしtitleがis => Maybe[Str](つまりis => 'Undef | Str')だった場合には、
defined $_[0]->title ? $_[0]->title . q{ } . $_[0]->name
: $_[0]->name;
となるでしょう。
各アトリビュートをより堅くする
例えば業務アプリケーションなどでは以下のようなことをすると思います。
nameはfirst_name(またはforename)とlast_name(またはfamily_nameやらsurnameやら)に分けるべき- それらの名前の文字長はDBスキーマの
varchar(XXX)などと足並みを揃えた制約を施す(subtypeを使う)と堅い titleはenum(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には、下記の余談にあるロール版とサブクラス版それぞれを例示するコードを保存しました。
続・triggerでclearerを指定する際の注意点
syouwa_eraでtrigger => \&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_deltaをrequiresすることになります)と、手を入れておくことも出来ます。
他の例
西暦と皇紀でも大体似たようなものですが、実際には旧暦などを考えなければいけないので気を付けてください。
拙作MooseX::Types::Locale::Language(ISO 639-1の言語コードの制約と型変換)でも、これと同じ状況の例をサンプルコードMooseX-Types-Locale-Language-0.003/examples/complex.plとして付属しています。なお、このモジュールについては別の機会に記事を書くつもりです。
もしclearerを使わないなら......
もしclearerを使わずに関係先のアトリビュートを直接その場で設定しようとするなら、以下のような厄介なことを解決しなければなりません。
- 無限ループを防ぐ仕組みを仕込まなければなりません
builderと同じ処理を書かなければなりません
無限ループについてですが、これは以下のようにして発生します。
$era->imperial_era(1);で元号を設定する- 元号の
triggerによって、$self->christian_era( $self->imperial_era + $Delta );などとして西暦を設定する - 西暦の
triggerによって、$self->imperial_era( $self->christian_era - $Delta );などとして元号を設定する - 2.に戻る
これを防ぐ方法は、例えば以下の通りです。
- セッターメソッドをアトリビュート名とは別の名称にする
- トリガーには匿名でないサブルーチンリファレンスを設定する
- 呼び元のメソッド名を見て、ループを断ち切る
具体的な動きとしては、以下のようになります。
$era->_set_imperial_era(1);で元号を設定する- 元号の
triggerで$self->_build_by_imperial_eraを呼ぶ - 2つ前のメソッドが
_set_christian_eraではないので、$self->_set_christian_era( $self->imperial_era + $Delta );として西暦を設定する - 西暦の
triggerで$self->_build_by_christian_eraを呼ぶ - 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. まとめらしくないまとめ
triggerとclearerとlazyを組み合わせて使う、それが要点です。
上記5.3.では、triggerとclearerとlazyの組み合わせを知らなかった時期の醜悪なコードを恥を忍んで晒しました。私のようなMoose初心者の方には、これを反面教師としていただけるかも知れません。
Mooseは大変強力です。Mooseを使ってプログラミングする時、「これって気持ち悪い書き方だよね」と思ったら、Moose::ManualやMoose::Cookbookを漁りましょう。きっと洗練された素直な書き方が出来るはずです。
無駄なことを頑張って仕上げるようなガチムチな仕事は避けましょう。大いなる自戒を込めて......。
追記 : 三宅さん(nekoyaさん)のはてブコメントで、triggerは一つしか書けないけど、afterは追加できるという違いは大きいと思う
という指摘をいただきました。単に導出先の値をリセットしたいだけならばclearerを使えば済みますが、ロールなどでtriggerされるメソッドの挙動をいじりたい場合には、メソッドモディファイヤーも有用ですね。なお、メソッドモディファイヤーをclearerのメソッドに適用するという合わせ技も出来ますので、夢が拡がります。
コメントする