Mooseでもっと簡単に!
ここまでClass::MOPによるクラス定義を紹介してきましたが、クラスを定義するためだけにリスト1のようなコードを書くのは冗長です。そこでメタレイヤ定義に必要な決まり文句的なコードの宣言を隠蔽したシンタックスシュガーや、さまざまなデフォルト動作を組み込んだツールが作られました。それがMooseです。
クラスの定義
ではさっそく先ほどMOPで直接定義したクラスをMooseで再定義してみましょう(リスト5)。
リスト5(1)でuse Moose
と宣言すると、Personクラス用のメタクラスを生成するほか、このメタクラスに対して簡単に操作を行えるようにいくつかの関数をエクスポートします。strict
とwarnings
(警告)も自動的にエクスポートされます。
アトリビュート作成はリスト5(2)のhas
を使うと$meta->add_attribute()
を呼んでいるのと同等になり、name
アトリビュートが生成されます。引数のis => 'rw'
はadd_attribute
の際にaccessor
引数を渡しているのと同等になり、読み書き可能なアクセサを作成することを意味します。また、Moose::Meta::Attributeではrequired
を指定することによってオブジェクト生成時にこのアトリビュートが引数で渡されていることが必須になります。
リスト5(3)のメソッド定義では特に何かを変える必要はありません。subでメソッドを定義するだけでMOPに自動的に$meta->add_method
が呼ばれたのと同じ状態になります。
Mooseを使用している場合、リスト6のようにして得られるメタクラスオブジェクトはClass::MOP::Class型ではなくMoose::Meta::Class型であることに注意してください。ただし実装としてはMoose::Meta::ClassはClass::MOP::Classを継承していますので、ロールなどの追加機能以外は互換性のあるAPIとなっています。
クラスの継承
MooseでPersonクラスが再定義できたので、今度はEmployeeクラスを定義してみましょう(リスト7)。
Employeeクラスでは親クラスの指定が必要ですので、リスト7(1)で親クラスを指定します。extends
は$meta->superclasses
と同等になります。
リスト7(2)でemployerアトリビュートを追加していますが、今度はここにisa引数を指定しています。isa
を追加すると動的な型制約を指定できます。
最後にリスト7(3)でdescribe
にメソッドモディファイアを追加しています。Class::MOP版に比べると冗長だったadd_before_method_modifier
の呼び出しがぐっとシンプルになりました。
MOPとMooseの使い分け
MOPを直接使ってもMooseのようなシンタックスシュガーを使っても、結果的に生成されるクラスの機能は変わりませんが、通常はより定義が簡潔なMooseを使うほうがよいでしょう。機械的に複数のクラスを生成したり、高度なカスタマイズを行う場合はMOPを直接使ったほうが有効でしょう。
なおMooseは比較的好き嫌いが分かれるツールですが、Moose をお手本にして同様の機能を提供するMouseやMooなどのモジュールも開発されています。それらのモジュールは、高速化のためにMOPの一部機能だけを提供しつつもMooseと似たシンタックスシュガーを提供するようになっています。
メタクラスの拡張
ここまでMOPで通常のクラスを定義する方法を紹介してきました。ですが、メタクラスはオブジェクトですので、このオブジェクト群を拡張して適用することで、さらにさまざまな応用ができます。たとえばhas()
でアトリビュートを定義したときに作成されるアクセサの挙動を変えたり、メタレイヤにそのクラスに関するメタ情報を保存したりできます。ここでは拡張を行う方法を簡単に紹介します。
メタクラスの定義
たとえば、任意のクラスで作成されたオブジェクトインスタンスを追跡するメタクラスを実装できます。オブジェクトがいつどこで作成・破棄されているかを監視するためのデバッグツールを作る場合、もしくはすべてのオブジェクトインスタンスを管理するようなもっと複雑な拡張などを作る場合にとても便利です。
この動作を実装するInstanceTrackerをリスト8に定義します。
リスト8(1)でMoose::Meta::Classを継承しました。さらにリスト8(2)でこのメタクラスから生成したオブジェクトインスタンスを格納しておくためのinstances
アトリビュートも宣言します。
このメタクラスのキモはリスト8(3)で、_construct_instance
をオーバーライドしてなおかつインスタンスを保存しています。これでメタクラス経由ですべてのオブジェクトインスタンスを見つけることができます。
InstanceTrackerメタクラスの利用
ではInstanceTrackerを使ってみましょう(リスト9)。
リスト9 定義したメタクラスの利用
リスト9ではUserというクラスのインスタンスをすべて保存することにしました。InstanceTrackerメタクラスをこのUserクラスに適用するには、use Moose
する際にリスト9(1)のように-metaclass
引数にメタクラスの名前を渡すだけでOKです。
-metaclass
宣言が適用されたクラスは、指定されたメタクラスを使って表現されるようになります。リスト9(2)でオブジェクトを生成するときも裏でInstanceTrackerメタクラスが使用され、最終的にコンストラクタ内でオーバーライドされた_construct_instance
が実行されてメタクラス内のinstances
アトリビュートに新しいオブジェクトが格納されてから返されます。
あとはUser->meta
からメタクラスを取得して、instances
の中身に操作を加えることができます。リスト9(3)ではとりあえずすべてのユーザの名前を小文字に変換してみましたが、必要であれば同じようなロジックでデータベース移行だろうが一貫性チェックだろうが、好きなことができます。
CPANに登録されている拡張モジュール
CPANには前節で解説したメタクラスの拡張を利用したモジュールがたくさんあります。ここでいくつかを紹介しましょう。
MooseX::StrictConstructor─引数の厳密な確認
Perlでは、期待されている引数以上の引数をオブジェクトコンストラクタに渡した場合、それを無視するのが一般的です。でもこれはバグの温床になりかねません。
リスト10のWidgetクラスではリスト10(1)でwrite_log
アトリビュートを定義したので、Widgetのコンストラクタにwrite_log
という引数を渡せるはずです。しかしリスト10(3)で間違ったuse_logという引数を渡してしまいました。そしてリスト10(2)で標準エラーに出力をするはずだと思っているのに何も起こりません。
通常クラス定義とオブジェクト生成のコードは別のファイルにあるため、クラス定義を確認しないとわかりにくいこのような問題は、とてもデバッグしにくいものになってしまいます。
これを避けるためのコードを通常のPerlのみで書くこともできますが、各クラスでアトリビュートをハードコードする必要があったりとあまり汎用性はありません。Moose/MOPを使えばイントロスペクションを利用してアトリビュートのリストを自動的に検知したうえで、引数の確認ができます。
この機能を実装しているのがMooseX::StrictConstructorです。このモジュールを使用するだけで、メタクラスを継承したメタクラスを提供してアトリビュートのリストと引数を照合するコンストラクタが作成されます。
リスト11(1)のようにuse MooseX::StrictConstructor
を追加すると、リスト11(2)でコンストラクタに不明なuse_log
という引数を渡すことはエラーになります。エラーがバグの原因行を指摘してくれるので便利です。
簡単なモジュールですが、たくさんの人が利用しています。MOPなしでは、このように手軽にコンストラクタを拡張することはできなかったでしょう。
MooseX::Method::Signatures─メソッド引数を定義できるPerl!?
Perlでは、リスト12のように関数が自らの引数を処理する必要があります。
ですが最近では、Devel::Declareを利用することでメソッドにシグネチャを加える自然なシンタックスシュガーを提供することが可能になりました。さらにこれをMooseと結合させるためにMOPを利用しているのがMooseX::Method::Signaturesです。
MooseX::Method::Signaturesは、Perl 6と似ているシグネチャを提供します。このモジュールからエクスポートされるmethodというキーワードを使うことで、メソッドのシグネチャを宣言して引数から変数までの割り当てを自動的に行えるうえ、オブジェクト本体である第1引数も自動的に$selfという変数に割り当てられます。
シグネチャの指定
リスト13では、Mac OS Xに付属のsayというコマンドラインツール(与えたテキストを発声してくれる)を使って何か喋らせるオブジェクトを定義しています。
リスト13(1)で、method
キーワードでメソッドを宣言します。ここでは引数なしのメソッドを定義しています。
リスト13(2)ではInt
型の$delay
という引数を宣言します。リスト13(3)のようにメソッド内で特にmy $self
やmy $delay
指定をせずとも、それぞれの変数が使用できるようになっています。
リスト13(4)では$wpm
という引数に160というデフォルト値を指定しています。
なお、リスト13(2)では位置固定の引数を指定しているのでリスト13(6)のように引数を渡しますが、リスト13(4)は変数の前に:を記すことにより「名前付き引数」として指定されています。このように指定された関数の引数はリスト13(5)のようにハッシュ形式で利用します。
裏で何が起こっているのか
このようにメソッドシグネチャが利用できるようになる裏側では、実はシグネチャのメタデータはメタレイヤに保存されていて、これをもとに本来のPerlのしくみに展開してメソッドを生成してくれています。
もしあとからさらにシグネチャを調べて何か操作をしたければ、メタクラスからこれらの情報を得ることができます(リスト14)。
リスト14(1)でget_method
で定義したメソッドshutdown
のメタレイヤを取得するとMooseX::Method::Signatures::Meta::MethodというMoose::Meta::Methodクラスを継承したオブジェクトを返します。リスト14(2)でそのサブクラスに追加したメソッドでシグネチャの文字列を取得できます。
文字列だけではあまり使い道がありませんが、リスト14(3)のようにparsed_signature
アトリビュートを取得すれば、シグネチャを表現するオブジェクトが得られ、このメソッドが位置固定の引数を期待しているのか、名前付き引数を期待しているのかなどの情報を得ることができます。
またリスト14(4)のようにそれぞれの引数を表現するオブジェクトも取得できます。これらのオブジェクトは、引数がどのような変数に代入されるのかや、型の情報などが得られます。さらに拡張を書く場合などは、この情報を使っていろんなことができそうですね!