モダンPerlの世界へようこそ

第4回Any::Moose:なにがどうでもムースはムース

CPANTSは情報の宝庫

Perlを使う最大の利点といわれるCPANですが、CPANは単なるモジュール置き場ではありません。CPANはまたPerlの利用状況を知るうえで不可欠な統計情報を得る場でもあります。そのような統計情報のいくつかは、いわゆるCPAN検索サイトからも確認できますが、より突っ込んだ情報が欲しい場合はCPANTSCPAN Testing Serviceと呼ばれるサイトを確認するのが便利です。

国内ではnipotanこと谷口公一氏が始めた輝け!全日本最強 CPAN Author 決定選手権のネタ元として知られていますが、このサイトでは個々の作者やモジュールの品質だけでなく、そのモジュールが実際にどこで使われているかという情報を得ることもできます。

たとえば前回取り上げたロール関連のモジュールの利用状況を調べてみると、古き良きExporterを依存モジュールとして取り上げているモジュールの数は275個。ルーク・パーマー氏のClass::Roleを利用しているモジュールはなし。クロマティック氏のClass::Rolesを利用しているモジュールは8個(そのうちご当人のモジュールが5個⁠⁠。前回大きく取り上げたスティーヴン・リトル氏とカーティス・ポー氏のClass::Traitは1個。スティーヴン・リトル氏のPerl6::MetaModelはCPANに登録されていないのでデータなしという具合に、Moose以前のロール関連モジュールはプロトタイプとしての価値しかなかったことがデータからも浮き彫りになるのですが、前回最後に取り上げたClass::MOPとMooseのほうは、この原稿を書いている2009年3月末の時点で、Class::MOPが24個Mooseにいたっては100人を越す作者の手になる382個ものモジュールが利用していて、その数はいまも日々増え続けているのですから、少なくともMooseはもう「ギークのおもちゃ」のレベルを超えて、実用段階に入ってきていることが読みとれるといってよいでしょう。

地上でもっとも「重い」シカ

ただ、そんなMooseにも泣き所はいくつかあります。

Wikipediaによると、Moose(ヘラジカ)は平均的な雄で体重380~720キログラム。地上でもっとも重いシカであり、アメリカやヨーロッパの陸上生物としてはバイソンに次ぐ大きさを誇るそうですが、PerlモジュールとしてのMooseも、CPAN界最大というほどではないにせよ、大きく、重たい、というのもそのひとつです。

もっとも、その重さの大部分は、Mooseの影に隠れているメタクラスがアトリビュートのアクセサやロールのメソッドなどを整理してオブジェクトを構築するときのコストによるもの。オブジェクトの構築法が変わっただけで、オブジェクトそのものは従来のPerl 5オブジェクトですから、準備ができたあとの実行速度は従来の方式で組んだオブジェクトと理論上差はありません。

ためしにこのようなスクリプトを実行してみてください。

package MooseObj;
use Moose;
has 'name' => (is => 'rw', isa => 'Str', default => 'foo');
sub print { my $self = shift; print $self->name; }

package main;
use Data::Dumper;
local $Data::Dumper::Indent = 1;
print Dumper(MooseObj->new);

ふつうにオブジェクトをダンプする限り、従来のオブジェクトをダンプしたときと同じ結果しか返ってきませんが、

$VAR1 = bless( {
  'name' => 'foo'
}, 'MooseObj' );

最後の行をこう変えると――あまりに長いので掲載はしませんが――ダンプされる行はたちまち100行ほどにふくれあがります。

print Dumper(MooseObj->new->meta);

Mooseが管理している範囲

このように、ひとつのオブジェクトをつくるだけで芋蔓式に大量のメタオブジェクトができてしまうことが、Mooseが重いといわれる原因のひとつなのですが、そのメタオブジェクトがもたらす自由をフルに活用している(あるいは、できる)ようなオブジェクト/モジュールというのは、それほど多くはありません。

もう一度、先ほどのメタオブジェクトをダンプした結果を確認してみてください。

Moose::Meta::Classのオブジェクト内にmethodsという項目がありますが、そこに入っているのはパッケージ内で明示的に指定したnameというアクセサと、Mooseのほうで自動的に追加するmetaというメソッドだけ。MooseObjパッケージに直書きした「printメソッド」の姿はありません。

  'methods' => {
    'name' => $VAR1->{'_meta_instance'}{'attributes'}[0]{'associated_methods'}[0],
    'meta' => bless( {
      'body' => sub { "DUMMY" },
      'associated_metaclass' => $VAR1,
      'name' => 'meta',
      'package_name' => 'MooseObj'
    }, 'Moose::Meta::Method' )

もちろんこれはこれでPerl 5のオブジェクト機構としては正しい挙動です。ここではあえて「printメソッド」と呼びましたが、これがMooseObjのメソッドであると言えるのは、私たちが$selfという文字を見てメソッドであると判断しているから。Perlにしてみれば、これだけではオブジェクトとは関係のないパッケージ関数なのか、オブジェクトに紐付いているメソッドなのか、判断しようがありません。

だから、本当にこのprint関数をMooseObjのメソッドとして扱いたければ、Mooseのロールをつくってwithコマンドで取り込むか(こうするとMooseが内部でロール内の関数をオブジェクトのメソッドとして登録してくれます⁠⁠、あるいは自分でメタオブジェクトを操作して、メソッドを追加するしかありません。

先の例でいうと、たとえばこのような書き方をすれば、MooseObjのメタオブジェクトをダンプしたときにprintというメソッドが確認できるようになります[1]⁠。

package MooseObj::Role::Print;
use Moose::Role;
sub print { my $self = shift; print $self->name; }

package MooseObj;
use Moose; with 'MooseObj::Role::Print';
has 'name' => (is => 'rw', isa => 'Str', default => 'foo');

アクセサ生成に特化したMouse

ただ、実際にこのようなことまで意識しながらMooseを使っている人はかなりの少数派。一般的には最初の例のように、アトリビュートの部分のみMooseで書いて、メソッドはMooseの機能を使わず、従来のやり方でクラスにベタ書きしてしまうことがほとんどです。

もちろんそれがいけないというわけではありませんし、このように既存のオブジェクトに簡単に組み込めるところがMooseの利点でもあるのですが、Mooseをアクセサ生成モジュールとしてしか使わないのであれば、わざわざこれほど大量のメタオブジェクトを生成する必要はありません。同じようなAPIを提供するにしても、もっと軽くて速い方法があるはずです。

そのような認識のもとで登場したのが、ショーン・ムーア(Shawn M. Moore)氏のMouseでした(2008年6月⁠⁠。

この当時のMouseは本当に機能が限られていて、hasによるアクセサ生成とextendsによる継承くらいしか対応していなかったのですが、メーリングリスト上の報告を鵜呑みにするなら、同じテストをMooseで実行した場合に比べて3分の1ほどの実行時間で済んだというのですからなかなかの高速化ぶりです。

どのくらい変わったかを見るために、先ほどのサンプルに少々手を加えてダンプしてみましょう。

package MouseObj;
use Mouse;
has 'name' => (is => 'rw', isa => 'Str', default => 'foo');
sub print { my $self = shift; print $self->name; }

package main;
use Data::Dumper;
local $Data::Dumper::Indent = 1;
print Dumper(MouseObj->new->meta);

100行近くにもなったMooseのメタオブジェクトに比べて、Mouseのメタオブジェクトはたったのこれだけ。

$VAR1 = bless( {
  'superclasses' => [
    'Mouse::Object'
  ],
  'name' => 'MouseObj',
  'attributes' => {
    'name' => bless( {
      'init_arg' => 'name',
      'name' => 'name',
      'class' => 'MouseObj',
      'default' => 'foo',
      'is' => 'rw',
      'type_constraint' => 'Str'
    }, 'Mouse::Meta::Attribute' )
  }
}, 'Mouse::Meta::Class' );

これならオブジェクトの生成が3倍速くなったというのもうなずけるところです。

牧歌的な世界

このようなコードベースの異なる機能限定版は、管理コストが上昇するためいやがられることもありますが、さいわい、Mouseの場合は、Mooseの作者スティーヴン・リトル氏からも好意的に迎えられました。

その年のYAPC::AsiaでMooseの発表を行ったnothingmuchことユーヴァル・コグマン(Yuval Kogman)氏からは、⁠気に入ったから使いたいけれど、あとから文句が出るといけないから」という理由で、コードをいじらなくてもMooseとMouseを切り替えられるようにする(ロード時にすでにMooseがロードされていたらMooseとして振る舞い、MooseがいなければMouseを呼び出してMouseとして振る舞う)Squirrel(リス)というモジュールを贈られています。

SquirrelのPODには「Moose(ヘラジカ)とSquirrel(リス)は大の仲良し。でも、Mooseがいないのであれば、SquirrelはMouse(ネズミ)とも楽しくやれるよ」という記述が見られるのですが、この一文が暗示するように、この頃のMooseの世界は、さまざまな動物が仲良く暮らしている、実に牧歌的な世界でした。

シカ事変

ところが、2008年も後半になると、その牧歌的な世界にいささかぎすぎすした空気が流れるようになってきます。ユーヴァル・コグマン氏が危惧していたように、Mooseを大事にしすぎるあまり、⁠あとから文句が出る」ようになったためです。

同年9月にリリースされたゼバスティアン・リーデル(Sebastian Riedel)氏のMojoに対する攻撃については以前別の特集記事でちらと紹介しましたが、同年春に開催されたYAPC::Asia以降Mooseベースに切り替わっていたHTTP::Engineも、11月に開催されたHTTP::Engine ConferenceやShibuya Perl Mongersのテクニカルトークからの流れで、高速化のためにMooseをやめ、同じようなインタフェースを持つShika(鹿)という国産モジュールを採用した途端に、一部のMoose開発陣から強い非難を寄せられました[2]⁠。

このときは、YAPC::Asiaの常連であり、ムーア氏の上司にもあたるジェシー・ヴィンセント(Jesse Vincent)氏らの取りなしもあって、Shikaのかわりに、すでに軽量化モジュールとして名前が知られていたMouseを使うことで決着がついたのですが、もともとカジュアルな用途で使うことしか想定していなかった当時のMouseは、バリバリのMooseベースで書かれていたHTTP::Engineをサポートするにはあまりに力不足でした。

そこで、HTTP::Engineチームが、Shikaで得られた知見を活かしながら、Mouseにどんどん機能を追加していった結果――もともとMouseは、できないことがあればMooseに切り替えればよい、という割り切ったスタンスでつくられていたのですが――いまではよほど高度なメタオブジェクト操作を行うのでもない限り、Mouseひとつで事足りてしまうようになりました(細かなところで本家と整合性がとれていない箇所は残っていますが、日常的に使うレベルではわざわざMooseを呼ぶ必要を感じないくらい完成度も互換性も高いものに仕上がっています⁠⁠。

Any::Moose

ただし、Mooseの世界におけるMouseの位置づけは、あくまでもライトユーザ向けの代替品にすぎません。Mooseがロードされている環境ではメモリの無駄遣いになるので使うべきではないとされていますし、今後、Mooseがさらに成長して高速化が進むようなら、開発リソースの無駄遣いということでMouse不要論が出てこないとも限りません。

そのため、過渡期のいまは、Mooseに対する配慮として、Mouseを直接使うのではなくAny::Mooseというラッパを一枚挟んでおくことが奨励されています。

このAny::Mooseは、趣旨としてはSquirrelと同等のものですが、Mouseが高機能化して、単純なMoose/Mouseの振り分けでは対応しきれなくなったため、あらためて両者の橋渡しをするために用意されたもの。

Moose/Mouse本体だけでなく、ロールや、MooseX以下の拡張モジュールなどの振り分けにも対応しているのが特徴で、たとえば前回利用したBatクラスであれば、このように書き直せます[3]⁠。

use strict;
use warnings;
use Test::More tests => 4;

package Fly;
use Any::Moose '::Role'; requires 'fly_with';
sub fly {
    my $self = shift;
    print "I can fly with ".$self->fly_with.".\n";
}

package Mammal;
use Any::Moose;
sub produce_milk { print "I can produce milk.\n"; }

package Bat;
use Any::Moose; extends 'Mammal'; with 'Fly';
sub fly_with { 'wings'; }

package main;

my $bat = Bat->new;

can_ok($bat => 'produce_milk'); # ok
can_ok($bat => 'fly');          # ok
ok($bat->isa('Mammal'));        # ok
ok(!$bat->isa('Bird'));         # ok

必要な部分のみ使えばよいのです

MooseはよくPerl 5のオブジェクト機構に対する革命であるといわれます。実際、その気になって使えば、メソッドと単なる関数を区別したり、アトリビュートに型制約をつけたり、ここでは説明しませんでしたが、第2回で見たやり方とはひと味異なる柔軟なメソッドの修飾を行ったりと、実にこだわった書き方ができるようになるのですが、Mooseは、誕生前夜に一世を風靡していた(ダミアン・コンウェイ氏のPerlベストプラクティスなどで紹介されている)インサイドアウト式のアトリビュートなどは採用していません。あくまでもごくふつうのPerl 5オブジェクトのなかに、必要な部分のみ組み込んで使えるようにつくられています。

今回紹介した初期のMouseのように、hasとextendsだけ利用するのもよいでしょうし、前回紹介したようにロールにこだわって使うのもよいでしょう。

Mooseも、別の特集記事で紹介したMojoと並んで、いま、Perl Foundationの助成金でドキュメントの拡充をはかっています[4]⁠。その一部はすでにMoose本体のディストリビューションに収録されて公開されていますが、予定では今後まだまだドキュメントが増えることになっていますので、これまで敬遠していた方も、これを機会にぜひ一度Moose/Mouseの世界に触れてみていただければと思います。

おすすめ記事

記事・ニュース一覧