北海道苫小牧市出身の初老PGが書くブログ

永遠のプログラマを夢見る、苫小牧市出身のおじさんのちらしの裏

Moose::RoleはJavaのInterfaceなんかじゃない

Moose::RoleはJavaのInterfaceと似たような物だと思ってたんですが、大きな誤解でした。

モダンPerlの世界へようこそを読んで、Moose::RoleはTraits: Composable Units of Behaviorの概念の実装らしいことがわかったので、この論文を読んでみました。*1。非常に面白い内容でした。P.12 の a) と b) を見るだけでも、この概念の面白さが伝わるんじゃないかと。要は、指定した振る舞い(requires)から新しい振る舞い(provides)を作るものが、Traitsってことです。(ただし、ここで言う振る舞いにはアクセサを含みます。)

誤解していたこと

Moose::RoleをTraitsとして見なすとすれば、JavaのInterfaceの性質である以下の2点は誤解です*2

  • Moose::Roleは、単なるインタフェース(API)定義ではない
    • ロジックの実装を持つべきです。
  • requiresするのは、「このRoleができること」ではない
    • Role内のロジックの実装のために必要となるメソッドを、requiresに書きます*3

使ってみる

詳しい説明はMoose::Cookbook::Roles::Recipe1に先ほどの論文と似た例が載ってるのでこちらに譲りますが、お試しで適当にやってみます*4

合成してLoggingロールを作る

まず、 Println なんてロールを作ります。これは、writeメソッドがあるクラスに対して、改行付きでの出力を行う println メソッドを提供します。

package Println;
use Moose::Role;

requires 'write';

sub println{
	my $self = shift;
	$self->write(@_, "\n");
}

no  Moose::Role;

後、Logging と言う log を提供するロールを作ります。こいつは今作った println メソッドが使えるとありがたいので、先ほどの Println と合成(composite)して作ります*5。ちなみに、Logging は Println を合成してますので、 println メソッドを必要(requires)とはしません。←4/8修正:Printlnロールが改変されてprintlnが無くなる場合もあり得るので、requiresは必要です。

package Logging;
use Moose::Role;

with 'Println';

requires 'println';

sub log {
	my $self = shift;
	$self->println( scalar localtime() . ' ' . $_) for @_;
}
no  Moose::Role;
HelloWorldロールを作る

さてさて、ここで上記とは全く別に HelloWorld なんて Role を作ってみます。こいつは、 println メソッドがある(requires)クラスにhelloworldメソッドを付け加え(provides)ます。この println メソッドは、HelloWorldロールが「こんなメソッドあればいいなあ」と思っている(requires)だけで、前述のPrintlnロールとは全く関連がないことに注目して下さい。

package HelloWorld;
use Moose::Role;

requires 'println';

sub helloworld {
	my $self = shift;
	$self->println("Hello world!");
}

no  Moose::Role;
ロールをクラスに合成する

さて、それでは、MyAppクラスを作ります。このクラスは "Hello world!" を出力して そのことをロギングします。先ほどのロールを合成しましょう。

package MyApp;
use Moose;

with 'HelloWorld', 'Logging';

no  Moose;


package main;
my $myapp = MyApp->new;
$myapp->helloworld;
$myapp->log("said 'hello'");

完成!! これで MyApp は HelloWorldする能力とロギングする能力を兼ね備えているはずですので、動かしてみます。

'HelloWorld|Logging' requires the method 'write' to be implemented by 'MyApp' at XXXX.pm line YY

エラーが出ました。これは、requires されているメソッドのうち、writeが用意されてないと言うエラーです。ここで、HelloWorldロールでrequiresしている "println" が勝手に解決されているのが大きなポイントです。このように、requiresに指定されているものでも他のRoleが持っているメソッドがあれば勝手に解決してくれます。

では、言われた通りにMoose::Roleが欲しがっているメソッド(GLUE)である write を作りましょう。

package MyApp;
use Moose;

with 'HelloWorld', 'Logging';

sub write {
	my $self = shift;
	warn @_;
}

no  Moose;
hello world!
Sun Apr  5 21:10:33 2009 said 'hello'

きちんと動きました!

まとめ

「振る舞いから新たな振る舞いを作る」って考えさえわかれば、有用なMoose::Roleを作れます。作ったMoose::Roleは使いたい物を片っ端から合成すれば、それぞれのメソッドの解決は自動でやってくれます。後は、足りないメソッドを自分で実装するだけです。

Moose::Roleの「未実装のメソッドを教えてくれる」と言うJavaのInterfaceっぽい機能*6は、Traitsを実現するためのおまけでした。

*1:全部は読んでません、悪しからず。読んだ。

*2:もちろん、Moose::RoleはTraits以上の表現力を持っているので、必ずしも誤解なわけではない

*3:その副作用として、requiresに書かれたメソッドの実装も持つことになります

*4:言わなくてもわかりますが実用は完全に無視した遊びです。

*5:withで合成できます

*6:JavaのInterfaceの機能も、当然それが主眼ではないですが