Perl Hackers Hub

第13回メタオブジェクトプロトコル入門(2)

Mooseでもっと簡単に!

ここまでClass::MOPによるクラス定義を紹介してきましたが、クラスを定義するためだけにリスト1のようなコードを書くのは冗長です。そこでメタレイヤ定義に必要な決まり文句的なコードの宣言を隠蔽したシンタックスシュガーや、さまざまなデフォルト動作を組み込んだツールが作られました。それがMooseです。

クラスの定義

ではさっそく先ほどMOPで直接定義したクラスをMooseで再定義してみましょうリスト5⁠。

リスト5 Mooseによるクラスの定義
package Person;
use Moose; # (1)

# (2)アトリビュートの定義
has 'name' => (
    is => 'rw',
    required => 1,
);

# (3)メソッドの定義
sub describe {
    my $self = shift;
    my $name = $self->name;
    print "$name";
}

リスト5(1)use Mooseと宣言すると、Personクラス用のメタクラスを生成するほか、このメタクラスに対して簡単に操作を行えるようにいくつかの関数をエクスポートします。strictwarnings(警告)も自動的にエクスポートされます。

アトリビュート作成はリスト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となっています。

リスト6 メタクラスの親クラス
my $meta = Person->meta;
$meta->isa( 'Class::MOP::Class' ); # true!
$meta->isa( 'Moose::Meta::Class' ); # true!

クラスの継承

MooseでPersonクラスが再定義できたので、今度はEmployeeクラスを定義してみましょうリスト7⁠。

リスト7 Mooseによるサブクラスの定義
package Employee;
use Moose;
extends 'Person'; # (1)親クラス

has 'employer' => ( # (2)アトリビュート
    is => 'rw',
    isa => 'Company',
    required => 1,
);

before describe => sub { # (3)メソッドモディファイア
    my $self = shift;
    my $company = $self->employer->name;
    print "所属 $company, 社員名 ";
};

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 メタクラスの定義
package InstanceTracker;
use Moose;

# (1)Moose::Meta::Classの子クラスなので、これもメタクラスである
extends 'Moose::Meta::Class';

# (2)生成したインスタンスの配列
has instances => (
    is => 'ro',
    isa => 'ArrayRef',
    default => sub { [] },
);

# インスタンスを生成する継承したメソッドをオーバーライド
sub _construct_instance {
    my $self = shift;

    # Moose::Meta::Classの_construct_instance本体がインスタンスを返す
    my $instance = $self->next::method(@_);

    # (3)このインスタンスを見失わないようにする
    push @{ $self->instances }, $instance;

    # newにインスタンスを返る
    return $instance;
};

リスト8(1)でMoose::Meta::Classを継承しました。さらにリスト8(2)でこのメタクラスから生成したオブジェクトインスタンスを格納しておくためのinstancesアトリビュートも宣言します。

このメタクラスのキモはリスト8(3)で、_construct_instanceをオーバーライドしてなおかつインスタンスを保存しています。これでメタクラス経由ですべてのオブジェクトインスタンスを見つけることができます。

InstanceTrackerメタクラスの利用

ではInstanceTrackerを使ってみましょうリスト9⁠。

リスト9 定義したメタクラスの利用

package User;

# (1)カスタマイズしたメタクラスを利用宣言する
use Moose -metaclass => 'InstanceTracker';

has name => (
    is => 'rw',
    isa => 'Str',
    required => 1,
);

# (2)生成時に、InstanceTrackerの_construct_instanceに
# 登録したフックを実行する
my $me = User->new(name => "SARTAK");
my $friend = User->new(name => "DMAKI");

# (3)インスタンス全部を小文字の名前に変換する
for my $instance (@{ User->meta->instances }) {
    $instance->name( lc($instance->name) );
}

print $me->name; # sartak
print $friend->name; # dmaki

リスト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)で標準エラーに出力をするはずだと思っているのに何も起こりません。

リスト10 間違った引数を渡してしまう例
package Widget;
use Moose;

has write_log => ( # (1)
    is => 'rw',
    default => 0,
);

sub something {
    my $self = shift;
    if ($self->write_log) { # (2)
    warn "write_log";
    }
    else {
    warn "oops!";
    }
}

my $widget = Widget->new(use_log => 1); # (3)
$widget->something();

通常クラス定義とオブジェクト生成のコードは別のファイルにあるため、クラス定義を確認しないとわかりにくいこのような問題は、とてもデバッグしにくいものになってしまいます。

これを避けるためのコードを通常のPerlのみで書くこともできますが、各クラスでアトリビュートをハードコードする必要があったりとあまり汎用性はありません。Moose/MOPを使えばイントロスペクションを利用してアトリビュートのリストを自動的に検知したうえで、引数の確認ができます。

この機能を実装しているのがMooseX::StrictConstructorです。このモジュールを使用するだけで、メタクラスを継承したメタクラスを提供してアトリビュートのリストと引数を照合するコンストラクタが作成されます。

リスト11(1)のようにuse MooseX::StrictConstructorを追加すると、リスト11(2)でコンストラクタに不明なuse_logという引数を渡すことはエラーになります。エラーがバグの原因行を指摘してくれるので便利です。

リスト11 間違った引数をエラーにする
package Widget;
use Moose;
use MooseX::StrictConstructor; # (1)メタクラスを拡張する

has write_log => (
    is => 'rw',
    default => 0,
);

my $widget = Widget->new(use_log => 1); # (2)直接エラーを投げる
$widget->something();

簡単なモジュールですが、たくさんの人が利用しています。MOPなしでは、このように手軽にコンストラクタを拡張することはできなかったでしょう。

MooseX::Method::Signatures─メソッド引数を定義できるPerl!?

Perlでは、リスト12のように関数が自らの引数を処理する必要があります。

リスト12 従来の引数処理
sub foo {
    # 自分で引数 @_から変数に代入しなければならない
    my ($arg1, $arg2, $arg3) = @_;
    ...
}

ですが最近では、Devel::Declareを利用することでメソッドにシグネチャを加える自然なシンタックスシュガーを提供することが可能になりました。さらにこれをMooseと結合させるためにMOPを利用しているのがMooseX::Method::Signaturesです。

MooseX::Method::Signaturesは、Perl 6と似ているシグネチャを提供します。このモジュールからエクスポートされるmethodというキーワードを使うことで、メソッドのシグネチャを宣言して引数から変数までの割り当てを自動的に行えるうえ、オブジェクト本体である第1引数も自動的に$selfという変数に割り当てられます。

シグネチャの指定

リスト13では、Mac OS Xに付属のsayというコマンドラインツール(与えたテキストを発声してくれる)を使って何か喋らせるオブジェクトを定義しています。

リスト13 シグネチャを使用したクラスの定義
package Computer;
use Moose;
use MooseX::Method::Signatures;

# (1)シグネチャなしでもよい
method try_crash {
    die if rand(100) (2)$delayは位置個定の引数
method shutdown (Int $delay) {
    # (3)$selfも自動的に割り当てる
    $self->try_crash;
    sleep $delay;
    exit;
}

# (4)名前付き引数
method speak (Str :$text, Int :$wpm = 160) {
    system("say", "--rate=$wpm", $text);
}

my $laptop = Computer->new;
$laptop->try_crash;
$laptop->speak(text => "This is goodbye.", wpm => 500);
# (5)wpmは160のデフォルト
$laptop->speak(text => "Sayonara.");
# (6)shutdownの$delay変数は5にする
$laptop->shutdown(5);

リスト13(1)で、methodキーワードでメソッドを宣言します。ここでは引数なしのメソッドを定義しています。

リスト13(2)ではInt型の$delayという引数を宣言します。リスト13(3)のようにメソッド内で特にmy $selfmy $delay指定をせずとも、それぞれの変数が使用できるようになっています。

リスト13(4)では$wpmという引数に160というデフォルト値を指定しています。

なお、リスト13(2)では位置固定の引数を指定しているのでリスト13(6)のように引数を渡しますが、リスト13(4)は変数の前に:を記すことにより「名前付き引数」として指定されています。このように指定された関数の引数はリスト13(5)のようにハッシュ形式で利用します。

裏で何が起こっているのか

このようにメソッドシグネチャが利用できるようになる裏側では、実はシグネチャのメタデータはメタレイヤに保存されていて、これをもとに本来のPerlのしくみに展開してメソッドを生成してくれています。

もしあとからさらにシグネチャを調べて何か操作をしたければ、メタクラスからこれらの情報を得ることができますリスト14⁠。

リスト14 シグネチャを調べる
my $meta = Computer->meta;

# (1)MooseX::Method::Signatures::Meta::Methodのインスタンス
my $shutdown = $meta->get_method('shutdown');
$shutdown->signature; # (2)"(Int $delay)"
# (3)シグネチャを表現するオブジェクト
my $signature = $shutdown->parsed_signature;

# "1" -- positionalである引数がある
$signature->has_positional_params;
# "0" -- 名前付き引数がない
$signature->has_named_params;

# (4)各引数を表現するオブジェクトの配列
my @params = @{ $signature->positional_params };
my $delay = $params[0];

$delay->variable_name; # $delay
$delay->type_constraints->to_string; # "Int"

リスト14(1)get_methodで定義したメソッドshutdownのメタレイヤを取得するとMooseX::Method::Signatures::Meta::MethodというMoose::Meta::Methodクラスを継承したオブジェクトを返します。リスト14(2)でそのサブクラスに追加したメソッドでシグネチャの文字列を取得できます。

文字列だけではあまり使い道がありませんが、リスト14(3)のようにparsed_signatureアトリビュートを取得すれば、シグネチャを表現するオブジェクトが得られ、このメソッドが位置固定の引数を期待しているのか、名前付き引数を期待しているのかなどの情報を得ることができます。

またリスト14(4)のようにそれぞれの引数を表現するオブジェクトも取得できます。これらのオブジェクトは、引数がどのような変数に代入されるのかや、型の情報などが得られます。さらに拡張を書く場合などは、この情報を使っていろんなことができそうですね!

おすすめ記事

記事・ニュース一覧