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

第3回Moose::Role:役割単位のクラス分け

多重継承しないほうがよい場合

前回は多重継承を利用してクラスを拡張するときにありがちな問題と、そのひとつの解決策を見てきましたが、クラスにいくつかのメソッドを追加したいだけであれば、むしろ継承を利用しないほうがふさわしい場合もあります。

たとえば「コウモリ」というクラスを実装するとき、⁠乳を出す」というメソッドのために「ほ乳類」というクラスを、⁠空を飛ぶ」というメソッドのために「鳥類」というクラスを継承するのは――たしかにそれで当座の問題は解決するかもしれませんが――違和感が残ります。

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

package Mammal;
sub new { bless {}, shift; }
sub produce_milk { print "I can produce milk.\n"; }

package Bird;
sub new { bless {}, shift; }
sub fly { print "I can fly.\n"; }

package Bat;
use base qw(Mammal Bird);

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 でも、コウモリは本当に鳥類ですか!?

このような多重継承によるメソッド拡張の問題をPerlの世界ではどう解消してきたのか。今回は、その問題を追いかけていきましょう。

Exporterを使った解決策とその問題点

さて、上の例の場合、コウモリはほ乳類ですから、BatクラスとMammalクラスは純粋に「is a」の関係にあります。だから、問題となるのは、Batクラスに(Birdクラスと共通の)flyメソッドを追加する部分だけです。

Perl 5の場合、メソッドとサブルーチンに実質的な差はありません。よそで定義されたサブルーチンを取り込む方法としては、コアに入っているExporterモジュールとimportメソッドを使うのが古典的な手法でした。

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

package Mammal;
sub new { bless {}, shift; }
sub produce_milk { print "I can produce milk.\n"; }

package Bird;
use base 'Exporter';
our @EXPORT_OK = qw(fly);
sub new { bless {}, shift; }
sub fly { print "I can fly.\n"; }

package Bat;
Bird->import('fly');  # use Bird 'fly';
use base 'Mammal';

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 コウモリは鳥類ではなくなりました

そもそも鳥類の「飛ぶ能力」はほ乳類に貸し出せるようなものなのか? とか、コウモリは鳥類を使役(use)するから飛べるというのは論理的におかしくないか? という根本的な疑問を別にすれば、これでひとまずうまくいくように見えます。

でも、Birdクラスが、たとえばこうなっていたら、どうでしょう。

package Bird;
use base 'Exporter';
our @EXPORT_OK = qw(fly);
sub new { bless {}, shift; }
sub fly_with { 'wings'; }
sub fly {
    my $self = shift;
    print "I can fly with ".$self->fly_with.".\n";
}

こう変えても、先ほどのテストの範囲内ではうまく飛べるように見えます。でも、実際にコウモリを飛ばそうとすると、⁠Can't locate object method "fly_with" via package "Bat" 」という実行時エラーが発生します(BatはMammalしか継承していませんので、当然Birdクラスのfly_withメソッドは引き継がれません⁠⁠。

このような欠点があることから、前回も登場したダミアン・コンウェイ氏が、Perl 6の設計が始まった2000年に刊行した著書オブジェクト指向Perlマスターコースの中では、Exporterを利用する手法はばっさりと切り捨てられていました。

Exporterモジュールとimportサブルーチンは通常のPerlにおいては重要であるが、オブジェクト指向Perlではほとんど使用されない。これは、クラスから変数またはサブルーチンをエクスポートすることがオブジェクト指向のカプセル化原理に反するためである。

山根ドキュメンテーション訳p.76

Traits論文とその反響

ところが、2002年11月にナサニエル・シェルリ(Nathanael Schaerli)氏らがTraits: Composable Units of Behaviorという論文を発表すると、いささか風向きが変わってきます。

このトレート(trait⁠⁠、たまたまPerl 6では同じ言葉を別の意味で使っていたことから、Perlの世界では(ほとんど)同じものをロール(role、⁠役割⁠⁠)と呼んでいるのですが、ごくおおざっぱにいえば、このロールないしトレートとは、クラスからインスタンスの生成やプロパティの管理を行う部分を取り払ったメソッド集にすぎません。だから、いくつかの制約はあるものの、実質的にはクラスにメソッドをエキスポートするのと大差ない使い方をするのですが、2003年から2004年にかけて、ダミアン・コンウェイ氏らとともにPerl 6の設計に携わっていたメンバーが次々にこのロールの実装例をリリースしていきます[1]⁠。

メソッドを再利用するための工夫

従来のExporterを使った実装との違いはいくつかあるのですが、ロールを実装する上での最大のルールは、メソッドの再利用性を高めるため、オブジェクトの状態(データ)には極力依存しないようにする、ということです。

だから、ロール自身がデータを持つことは御法度ですし、ロールがオブジェクトからパラメータを受け取る際には、そのロールを利用するクラスに適切なアクセサメソッドの実装を強制しなければなりません(そのようなメソッドが存在していない場合は、⁠実行時ではなく)ロード時にエラーを出さなければなりません⁠⁠。また、ロールの中で指定したアクセサ以外の外部メソッドを使うのももちろんだめです。

たとえば、先ほどのfly_withメソッドを使う例をClass::Traitを使って実装してみましょう。ここでは@REQUIRESという変数を利用してこのロールを利用するクラスにfly_withメソッドの実装を強制しています。

package Fly;
use strict;
use warnings;
use Class::Trait 'base';
our @REQUIRES = qw(fly_with);
sub fly {
    my $self = shift;
    print "I can fly with ".$self->fly_with.".\n";
}

1;

そのFlyロールを利用するクラスの例はこうなります(ここでfly_withの実装を忘れると、ロード時に「Requirement (fly_with) for Fly not in Bat」というエラーが発生します⁠⁠。

package Bat;
use Class::Trait 'Fly';
use base 'Mammal';
sub fly_with { 'wings'; }  # この行がなければコンパイルエラー

これだけだとポイントが見えづらいかもしれませんので、もうひとつ、飛行機というクラスを用意してみましょう。

package Aircraft;
use Class::Trait 'Fly';
use base 'Transportation';
sub fly_with { shift->engine_type; }

オブジェクトという「名詞」は変わりましたが、flyという「動詞」の部分はいっさい変わっていません。このような動作と状態の分離がロールの肝です。

ロールに優先順位はありません

このように状態を持たないという性質から、ロールは、ロード順を気にせず、自由に組み合わせられるものでなければならないという決まりもあります。万一メソッドの衝突が起こった場合は、Exporterのようにロードした順にメソッドを上書きしていくのではなく、別名などを利用して衝突を回避しなければなりません。

たとえば「酪農」というロールにも「ほ乳類」クラスと同様にproduce_milkというメソッドがある場合を考えてみましょう。

package Dairying;
use strict;
use warnings;
use Class::Trait 'base';
our @REQUIRES = qw(animal);
sub produce_milk {
    my $self = shift;
    print "My ".$self->animal." produce milk.\n";
}

1;

このロールを利用して「酪農家」というクラスを作成すると、通常はベースクラスのメソッドが上書きされてしまいます。ただ、両者は名前こそ同じですが、意味(振る舞い)が異なりますから、ここでは別名を使って衝突を回避しましょう[2]⁠。

use strict;
use warnings;

package DairyFarmer;
use base 'Mammal';
use Class::Trait (
    Dairying => {
        alias   => { produce_milk => 'produce_cow_milk' },
        exclude => 'produce_milk',
    }
);
sub animal { 'cows'; }

package main;
my $farmer = DairyFarmer->new;
$farmer->produce_milk;          # I can produce milk.
$farmer->produce_cow_milk;      # My cows produce milk.

メソッドのつながりを知るためのメタモデル

このように、ロールという考え方は、慣れると非常に便利に使えるのですが、ロールの概念を忠実に実装しようとすると、どうしてもロールを利用するクラスの中身がどのような構造になっているか調べる必要が出てきます(前回紹介したSUPER疑似クラスやNEXTなどでメソッドチェーンをたどっていくだけでは衝突などの問題に対応しきれないためです⁠⁠。

もちろん従来のやり方でも、UNIVERSAL空間に実装されているcanやisaのようなメソッドを利用したり、⁠%::」という特殊な内部ハッシュを覗けばそれなりに情報は得られますし、2002年にリリースされたClass::Inspectorというモジュールを使えば、もう少しきれいな形で情報を取得することもできますが、そのようなPerl内部の情報をハックするよりは、クラスごとにどのようなクラスを継承していて、どのようなメソッドが実装されているかといったメタデータを持たせたほうが管理が楽です。

だから(実際にはそればかりが理由ではないのですが⁠⁠、2004年にまとめられたApocalypse 12というPerl 6オブジェクトの初期仕様では、このようなロールをサポートする仕掛けとして、Lispの世界で実績のあったメタクラスの導入がうたわれました。

Perl 6そのものの書き方については割愛しますが、スティーヴン・リトル氏がPerl 5で実装したPerl6::MetaModelというプロトタイプを利用すると、Batクラスはこのように表現できます[3]⁠。ロールの指定には「is」とか「has」でなく、⁠does」という(助)動詞を使っているところにも注目です(英語でBat does fly.と書くと、⁠コウモリは(本当に)飛べるんです」という意味になります⁠⁠。

use strict;
use warnings;
use Perl6::MetaModel;
use Perl6::Object;

role Fly => {
    methods => {
        fly => sub {
            my $self = shift;
            print "I can fly with ".$self->fly_with.".\n";
        }
    }
};

class Mammal => {
    is => [ 'Perl6::Object' ],
    instance => {
        methods => {
            produce_milk => sub { print "I can produce milk.\n"; }
        }
    }
};

class Bat => {
    is   => [ 'Mammal' ],
    does => [ 'Fly' ],
    instance => {
        methods => {
            fly_with => sub { 'wings' }
        }
    }
};

my $bat = Bat->new;
$bat->produce_milk;
$bat->fly;

Class::MOPとMooseの登場

ただ、このPerl6::MetaModelは、あくまでもPerl 6を実装する際の参考にするためにつくられたプロトタイプに過ぎません。CPANにはアップロードされていませんし、スティーヴン・リトル氏自身も引き続き趣の異なるPerl6::MetaModel 2.0の実装に取り組み、2005年の10月にはそちらを新たなPerl6::MetaModelとしているくらいで、まだあるべき姿は固まっていませんでした。

そのような試行錯誤に一段落がついて、より本格的なメタオブジェクトプロトコル(MOP)の実装として、同氏が満を持してリリースしたのが、Class::MOP(2006年2月)と、そのラッパーであるMoose(2006年3月)です。

Mooseを使うと、Batクラスはこのように表現できます。

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

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

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

package Bat;
use 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

このような小さな例ではありがたみがわかりづらいですが、一見するだけでも、Class::Traitの頃に比べて、@REQUIRESのようなグローバル変数が消え、コードそのものも(いくつかのキーワードのおかげで)すっきりしたのが感じられます。

MooseはPerl 5とPerl 6の架け橋

ただし、MooseはClass::Traitのようにロールのみを扱うモジュールではありません。詳細は次回以降にゆずりますが、Mooseはクラスの継承関係や、アトリビュート、メソッドの解決順序なども扱える大きなフレームワークであり、Perl6::MetaModelと同じく、Perl 5で実装されたPerl 6のプロトタイプであるともいえます。

だから、前回紹介したように、Perl 6の設計過程で生まれたNEXTがPerl 5.8で、mroがPerl 5.10でコアモジュール入りしたのと同じ流れで、Perl 5.12ではMooseとClass::MOPがコアモジュール入りしたとしても、おそらくだれも驚きません。

が、Mooseもまだまだ完璧とはいえません。次回は、Mooseの抱える問題と、それに対するいくつかの反応を紹介していきます。

おすすめ記事

記事・ニュース一覧