2008年05月05日

本日は Roppongi.PM の第一回 Moose コードリーディングがありました。

Mooseは、単純に使ってる分には分り易いのですが、その実装を見ようとすると途端に複雑さが増します。
とにかくメソッドの呼び出しのスタックが深い。MySQL程では無いにしろ曲者です。
今回はそんなMooseの挙動を把握する手がかりを掴もうという回です。

Moose.pm

Moose.pmは、主にuse Mooseされた時にexportするメソッドの定義をしています。
use Mooseすると、extends,with,has,before,after,around,override,inner,augment,make_immutable($c->meta->make_immutableすべき),confess,blessedがexportされます。
そしてMooseを実用的なパフォーマンスにするmake_immutableを、Moose::*のクラスに対して行ないます。
Mooseのコアクラス自信はimmutableなのです。

importメソッドの中で呼ばれるinit_metaが重要っぽくて、うまくまとめられないからリストだけする。
Moose->import;
- Moose::init_meta;
- $callerのpkgが登録されてるか調べる
-- find_type_constraint($caller_pkg) (実体は Moose::Util::TypeConstraints::find_type_constraint)
-- $caller_pkg が isa("Moose::Meta::TypeConstraint") だったら、$caller_pkgを戻す
-- じゃ無ければ
--- Moose::Meta::TypeConstraint::Registry->newしたインスタンスを Moose::Util::TypeConstraints が保持していて、そのインスタンスの->get_type_constraint($caller_pkg)
--- Moose::Meta::TypeConstraint::Registry のインスタンスの type_constraints が保持してるHASH refに$caller_pkgに対応する値を返す
- 登録なさげだったら登録する
-- Moose::Util::TypeConstraints::class_type($caller_pkg)
--- Moose::Util::TypeConstraints::create_class_type_constraint($caller_pkg)
--- class と name を $caller_pkg にしたhashを作って
---- Moose::Meta::TypeConstraint::Class->new(%hash)
---- ここで use Moose をしたクラスのクラスオブジェクトが作られるのです
---- _create_hand_optimized_type_constraint とか compile_type_constraint とかで、上で作ったオブジェクトの正統性をチェックする為のメソッドを作る。親クラスが居れば、それらの継承順位とかチェックしてるっぽ
--- 上で作ったオブジェクトを Moose::Util::TypeConstraints::register_type_constraint で登録
- Moose::init_metaに戻ってきた
- $caller_pkg->metaの正当性チェックするけど、そもそも meta method が無ければ $metaclass から meta class 作って $caller_pkg に meta method 生やすよ。詳細は下から
-- $metaclass は基本的に Moose::Meta::Class で Moose::Meta::Class->initialize($caller_pkg) で meta object 作る
-- Moose::Meta::Class の実体は Class::MOP::Class で Class::MOP::Class->initialize($caller_pkg, 'attribute_metaclass' => 'Moose::Meta::Attribute', 'method_metaclass' => 'Moose::Meta::Method', 'instance_metaclass' => 'Moose::Meta::Instance', @_) を実際呼んでる。
--- 一度作ったメタクラスは Class::MOP::get_metaclass_by_name($package_name) で取りだせる。 Class::MOP の %METAS に入ってる。
--- 1回も作ってなければ Class::MOP::Class->construct_class_instance して instance 作るよ
--- ちなみに Class::MOP::Class には make_immutable したときに作った anon class の後処理するための DESTROY method もあるよ!
- Moose::init_metaに戻ってきた
-- $meta->add_method で $caller_pkg に meta method を割り当てる
-- 実体は Class::MOP::Class->add_method( method_name, code ) になってるよ
-- code の部分は Class::MOP::Method の派生クラス(Moose::Meta::Method)や CODE リファレンスが指定できるよ。
-- CODE リファレンスの時は Moose::Meta::Method->wrap(code) で Moos::Meta::Method (Class::MOP::Method) オブジェクトを作るけどね
--- で add_package_symbol をするわけだが、これは Class::MOP::Method の親クラスの Class::MOP::Module の親クラスである Class::MOP::Package 樣に実装されている --- Class::MOP::Package->add_package_symbol( '&methodname' => code ) みたいにして登録する。&methodnameのぶぶんのsigilのチェックをしてるので&は必須だ
--- で、あとは一般的な手法の glob で method 生やす事をやっている。生やす対象は meta object が管理している class name (今の場合は $caller_pkg)だ
--- 最期に update_package_cache_flag を呼ぶのだ。add_method したときのおまじないか? Perl 5.10 世代じゃなきゃ Class::MOP の数少ない XS コードを使って PL_sub_generation という値をとって来てる。
--- 良く分らないけど get_method_map の中で使っていて(これはadd_methodするときに meta object に method を登録する部分で利用)を見て perldoc Class::MOP すると module の symbol table が更新されたかどうかが分るものっぽい
- Moose::init_metaに戻ってきた
- そんなこんなで、最期に superclasses がなければ superclasses を設定 (Moose::Object) して init_metaは終る
- superclasses の設定の中味は Class::MOP::Package->get_package_symbol('@ISA') とか使ってるのだ、簡単にいうと push @CLASSNAME::ISA, 'Moose::Object' と変わらないしょりだ

use Moose をするとこんな事になるわけだ。

meta method はどっからくるんだ?

use Mooseした側の meta method が生える仕組みは理解できましたが、その他の Moose::Meta::TypeConstraint::Registry とかにも、いつの間にか meta が生えてますね。
これは一体どこからくるのでしょうか?

答えは Class::MOP::Object にあります。
Moose::Meta::TypeConstraint::Registry の親クラスは Class::MOP::Object です。
Moose::Meta::TypeConstraint::Registry->meta を呼ぶと Class::MOP::Class された Moose::Meta::TypeConstraint::Registry のオブジェクトが帰るのです。

Moose の三賢者

Moose::Meta::Classの

sub initialize {
    my $class = shift;
    my $pkg   = shift;
    $class->SUPER::initialize($pkg,
        'attribute_metaclass' => 'Moose::Meta::Attribute',
        'method_metaclass'    => 'Moose::Meta::Method',
        'instance_metaclass'  => 'Moose::Meta::Instance',
        @_);
}
がそれ。
Moose::Meta::Method は既に出て来たので省いて、Moose::Meta::Instance (実体は Class::MOP::Instance) から。

いわゆる not Moose な時の ClassName->new を Class::MOP->construct_instance が行なっている。
実体は Class::MOP::Instance->new->create_instance あたり。
construct_instance で作った instance に attribute を設定している。

attribute とは、何か? それは Moose::Meta::Attribute まわりの事であり、ざっくり言うと accessor もうちょっと具体的に言うと has の実装にかかわっているもの。

Moose::Meta::Attribute

__PACKAGE__->meta->add_attribute したら引数が Class::MOP::Attribute を継承するオブジェクトだったら Class::MOP::Class->add_attribute に飛ぶ
そうじゃなければ Moose::Meta::Class->_process_attribute に go
attribute name の先頭に + が付いてるときは、親クラスの attribute を継承して部分的に上書き出来て、そうじゃなきゃ全部上書きだよ
そうこうして Moose::Meta::Attribute->new されるんだよ。
実際は Class::MOP::Attribute に処理飛ぶんだけど、 has と Class::MOP::Attribute のオプションって互換性無いから _process_options でオプションを変換してるんだ。

is => 'ro' を reader にして trigger が指定されてないか見たり。

is => 'rw' を accessor にして trigger が指定されていたら CODE リファレンスか確認したり。

とにかく has のオプションの値チェックは _process_options に詰まってるんだ。

それが終ったら Class::MOP::Attribute->new される。
といっても bless するだけで終っちゃうけどね。

で、その new された Moose::Meta::Attribute のオブジェクトが Class::MOP::Class->add_attribute に引数として渡される。
attach_to_class で、 attribute object に attribute を保持してる側の context を渡す。
has_attribute して、既に登録されてる attribute name だったら remove_attribute しちゃう

そして install_accessors Class::MOP::Attribute->install_accessors してから Moose::Meta::Attribute->install_accessors が実行される。
Class::MOP の方は、さらに accessor の種類ごとに process_accessors が呼ばれて、普通の時だと Moose::Meta::Method::Accessor (Class::MOP::Method::Accessor) のオブジェクトが new される。
accessor 用の Moose::Meta::Method だと思えばおk
Moose::Meta::Method::Accessor はすっごい文字列 eval がんばってるけど、あんま気にしないで。普通に使ってる分にはあんまし使われない気がしてる。

Moose::Meta::Attribute の install_accessors は、 handles の処理を行なってる。

has 'name' => ( ... handles => ... )
するときの初期化処理の部分だ。
_canonicalize_handles で delegation するメソッド一覧を作る。
handles の型によって処理内容が変わり、HASH, ARRAY は置いとくと、 Regexp, CODE は _find_delegate_metaclass を使って Moose::Meta::Class なオブジェクト出なければ、なんと Moose::Meta::Class で wrap されたオブジェクトを作るのだ。
あとは、それ以外のリファレンスの他に handles には Moose::Meta::Role を継承しているクラスが利用可能だ。Roleのメソッドをまるごとdelegationしちゃう。

たとえばこんな感じでつかえる。

use strict;
use warnings;
use Base;
use Class;
my $b = Base->new;
$b->foo(Class->new);
warn $b->foo;
warn $b->yappo;
package Base;
use Moose;
use Class;
has 'foo' => (
    is      => 'rw',
    does    => 'Role',
    handles => 'Role',
);
1;
package Role;
use Moose::Role;
requires 'yappo';
1;
package Class;
use Moose;
with 'Role';
sub yappo {
    warn "hoge";
}
1;

handles に Regexp リファレンス と CODE リファレンスを渡すことにより、delegation したいメソッドを指定する自由度が格段にあがる。
そして Role 使えば、 delegation したいメソッドのセットを使いまわせる。便利! でも Role の attributes は delegation できないっぽいす。
っぽいっていうか

        return map { $_ => $_ } (
            $role_meta->get_method_list,
            $role_meta->get_required_method_list
        );
attributeいれる処理ないお。

最期に get_attribute_map に attribute を登録して add_attribute は終了。

Role

Moose を使うなら Role しなきゃモグリってぐらい重要な要素。これは Class::MOP には無く Moose での実装のみです。

Role ってのは、クラスの定義を決めたり前述の handles に指定して delegation をまとめてやっちゃえる実装定義のセットみたいなもんす。

Role する時は use Moose::Role だけでおk、 use Moose したときと同等 (Roleに特化してるけど)なメソッドが提供されます。
Role なクラスの meta object は Moose::Meta::Role が担当してくれます。
Moose::Meta::Role の親クラスは Class::MOP::Module です。
Role は実装本体ではなく実装の定義をあつかうので add_method や add_attribute を呼び出しても glob 操作でメソッド生やすとかはしません。
メソッドの定義とかを追加するだけです。
requires の実装も Moose::Meta::Role の add_required_methods して accessor に情報ため込んでるだけです。

ため込んだ情報はいつ使うのでしょう?
そう Moose.pm で実装されてる with を使ったときだ(あとは has の does や handles でも見られるけど。。。)
with の実体は Moose::Util の apply_all_roles メソッドである。
その後どう処理するかは、 with の引数の内容によりけりだが、単純な with 'Role' の場合だと Moose::Meta::Role->apply が呼ばれる。
さらに with を呼び出した側のクラスが Role なのか何なのかにもよって変わってくる。
要するに Role を何処に適用するかで Moose::Meta::Role::Application::x の x の部分が変わる。

今回は簡単に、Moose なクラスに普通に Role を適用する場合をみる。
実装定義を充たしているかのチェックは Moose::Meta::Role::Application::ToClass と Moose::Meta::Role::Application を読むと良い。
処理内容は

sub apply {
    my $self = shift;

    $self->check_role_exclusions(@_); # role check
    $self->check_required_methods(@_); # method が定義されてるか
    $self->check_required_attributes(@_); # nop

    $self->apply_attributes(@_); # Role で定義されてる attribute を class に install
    $self->apply_methods(@_); # Role で定義されてる method を class に install

    $self->apply_override_method_modifiers(@_); # 以下略

    $self->apply_before_method_modifiers(@_);
    $self->apply_around_method_modifiers(@_);
    $self->apply_after_method_modifiers(@_);
}
こんな感じ。
なんだか check_required_attributes が未実装なんだけど、 attribute の定義も出来るようになるのかしら。

Role の中で has されたり sub foo {} されたり before, after, around, override されたものが全て適用されます。
もちろん with を使ってる側の meta object に、想像どうりのメソッドで追加されてきます。
Moose::Meta::Role::Application::ToRole なんてのもあるですが、それは Role の継承とほぼ等しいです。
ただし with は、 with した瞬間の Role をコピーするというイメージなので注意がいるかも。

before, after, around, override

最期に Moose を method hook する便利メソッドたち。(え? extends そんなの自分で読んで)

実体は Class::MOP::Class->add_(?:before|after|around)_method_modifierなんだ!
$fetch_and_prepare_method に入れられた CODE リファレンス (たぶん外部からどうあがいても弄れない実装にしたかったんだろう)を使って、hook したいメソッドを取りだす。
厳密には Class::MOP::Method::Wrapped のオブジェクトにして返す事をしている。
can を使わずに C3 な継承リストからメソッドを探すのがポイントかな。(これは Class::Component::Component::Moosenize でもやろう)
あとは、hook したいメソッドは Class::MOP::Method::Wrapped のオブジェクトにつつまれてるので、 hook point に対応したメソッドで hook すれば簡単に hook できちゃう。

よしOK、 Class::MOP::Method::Wrapped の中身を見てみよう。
Class::MOP::Method::Wrapped->wrap に、 wrap したいメソッドを入れて使うんだよね。
で、 wrap が呼ばれる度にメソッドの before, after, around のテーブルが書き換わって、 hook されたメソッドがちゃんと動くような anon method を作ってくれるんだ。
add_before_modifier, add_after_modifier なんかはスタック溜めるだけ、 add_around_modifier はちょっと複雑だけど、ちょっとしたくふうで a( b( c( orig() ) ) ) の構造でメソッドをつつんでくれるんだ。
元もとメソッドが定義されていたとしても glob 操作で Class::MOP::Method::Wrapped なメソッドで上書きしてくれるから安心設計だよ。
あと、1回 Class::MOP::Method::Wrapped->wrap されれば add_method されるので、 before, after, around なんかのスタックはちゃんと保持されてる。

ああ、そうだ override を忘れていた。
こいつは Moose::Meta::Class->add_override_method_modifier を使ってる。
そこから Moose::Meta::Method::Overriden を使ってるんだけど
これって使い道よくわからん、 extends したクラスのメソッドを上書きするのに使うみたいだ。
素のメソッド上書きやるよりは Moose 使ってるときは override して上書きしとけだと思うけど。
利点は @Moose::SUPER_ARGS に引数が入っていて $Moose::SUPER_BODY に親クラスのメソッドの CODE リファレンスが入ってる事かな。
と思ったら、 override で上書きしたメソッドの中で super() すると my $self = shift; $self->next::method(@_); と同等になるのか。

augment, inner は、 override, super の逆バージョンって事でおk

そのほか

has の時の値の set/get は Moose::Meta::Attribute の set_value/get_value を見よ。

Immutable

マジックみたいなもんだし、そもそも make_immutable しなくても良いのは小学生までだよねーと mst が言っていたので、ここは書かなくておk

さいごに

いつものように Moose まともに使ってないから、そこんとこよろしく。

Posted by Yappo at 2008年05月05日 17:57 | TrackBack | Perl
Comments
Post a comment









Remember personal info?






コメントを投稿する前に↓の場所にnospamと入力してください。