世界のナベアツ問題をMoose::Roleで解く

最近、Perlの勉強を始めて Moose というモジュールが面白かったので、これを使って「世界のナベアツ問題」を解いてみました。

世界のナベアツ問題」とはこれです。

1 から 40 までの数をプリントするプログラムを書け。 ただし 3 の倍数または数に 3 が含まれる時はその数に続けて、「〜〜〜」をプリントし、8 の倍数の時はその数に続けて「ぅぅういえぇえあ」をプリントすること。

これを普通にPerlで書くと、こんな感じになると思います。

use strict ;

sub print_number {
    my ($n) = @_ ;

    print $n ;

    print "〜〜〜" if (($n%3 == 0) || ($n =~ /3/)) ;
    print "ぅぅういえぇえあ" if ($n%8 == 0) ;

    print "\n" ;
}

print_number($_) for 1..40 ;

これだと、print_number という関数の中に、以下のような仕様が全部埋め込まれています。

  • まず数字を表示する
  • アホになる条件(3 の倍数または数に 3 が含まれる時)
  • アホになった時に付加する文字列(「〜〜〜」)
  • 快感になる条件(8 の倍数の時)
  • 快感になった時に付加する文字列(「ぅぅういえぇえあ」)

もちろんこんな短いプログラムでは問題になりませんが、実際のプログラムでは、ひとつの関数の中に多くの処理を詰めこんでしまうと、解読、修正、再利用が難しくなります。また、多人数で同時に開発する場合に、コミュニケーションのミスが発生します。

そこで、仕様をなるべく分割し、かつ、ドメイン(適用業務)に近い記述で表現するのが、オブジェクト指向です。Perlにもその機能があるし、Mooseも基本はそれを拡張したものです。

しかし、一般的なオブジェクト指向言語では、クラスという形で分割された仕様を実装し、継承という形でそれらをつなげることが必要です。そして、継承に関連して、プログラミングのレベルでいろいろ難しい問題が発生して、個々のクラスを独立させることが難しくなってしまいます。

Mooseには「ポストモダンオブジェクト指向」というスローガンがあって、Role という記述の単位によって、継承のわずらわしさを無くそうとしています。

たとえば、「ある条件の時だけ、数字の出力後に特定の文字列を付加する」という処理ををMoose::Roleで書くとこうなります。

package Nabeatu::Role::Aho ;
use Moose::Role ;

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

requires 'aho_condition' ;

after 'print_number' => sub {
    my ($self, $n) = @_ ;

    print $self->aho_string if $self->aho_condition($n) ;
};

Roleは単独では動作しません。必ず、一般のクラスに「混ぜて」使います。その使う側のクラスに対して、「〜〜〜」を付加するというような、特定の機能を提供します。

MooseのRoleが優れていると私が考えるのは、その使う側のクラスに対して、「このRoleを使いたかったら、こういう機能を用意しろ」ということを表現できることです。この Role では、以下のことを要求しています。

  • aho_stringという文字列の属性に値を設定すること
  • aho_conditionというメソッドを用意すること
  • print_numberというメソッドを用意すること

そして、print_numberが実行された時に、クラス側の処理の後に、aho_conditionが成立しているかどうかをチェックして、それが真の時にだけ、aho_stringを表示します。

同様に、別の条件で「気持ちいい」みたいな文字列を付加する Role を書くとこうなります。

package Nabeatu::Role::Ecstasy ;
use Moose::Role ;

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

requires 'ecstasy_condition' ;

after 'print_number' => sub {
    my ($self, $n) = @_ ;

    print $self->ecstasy_string if $self->ecstasy_condition($n) ;
};

それで、この二つのRoleされた要求を満たすクラスを書けばいいわけですが、ここでは、さらにもう少し工夫して、この要求を満たすRoleを作成してみます。

package Nabeatu::Role::Conditions ;
use Moose::Role ;

has 'aho_number' => (
    is => 'rw' ,
    isa => 'Int',
    required => 1,
    ) ;

has 'ecstasy_number' => (
    is => 'rw' ,
    isa => 'Int',
    required => 1,
    ) ;


sub dividable_by {
    my ($m, $n) = @_ ;
    return ($m%$n) == 0 ; 
}

sub has_number_character {
    my ($m, $n) = @_ ;

    return $m =~ /$n/ ;
}

sub aho_condition {
    my ($self, $n) = @_ ;

    return dividable_by($n, $self->aho_number) || has_number_character($n, $self->aho_number) ;
}

sub ecstasy_condition {
    my ($self, $n) = @_ ;

    return dividable_by($n, $self->ecstasy_number) ;
}

この Roleは、aho_conditionとecstasy_conditionという二つのメソッドを提供しますが、その代わりに、aho_numberとecstasy_numberという二つの属性を要求します。つまり、この「○で割りきれる」という基準となる数字を、使うクラスの側で設定できるようにします。

そして、上記の3つのRoleを使って、ナベアツを表現するクラスを書くとこうなります。

package Nabeatu ;
use Moose; 
with qw(Nabeatu::Role::Aho Nabeatu::Role::Ecstasy Nabeatu::Role::Conditions) ;

sub BUILDARGS {
    my ($class, %args) = @_ ;

    $args{aho_string} ||= "〜〜〜" ;
    $args{ecstasy_string} ||= "ぅぅういえぇえあ" ;
    $args{aho_number} ||= 3 ;
    $args{ecstasy_number} ||= 8 ;

    return $class->SUPER::BUILDARGS(%args) ;
}

sub print_number {
    my ($self, $n) = @_ ;

    print $n ;
}

after 'print_number' => sub {
    my ($self, $n) = @_ ;

    print "\n" ;
};

my $nabeatu = Nabeatu->new ;
$nabeatu->print_number($_) for (1..40) ;

実行結果

1
2
3〜〜〜
4
5
6〜〜〜
7
8ぅぅういえぇえあ
9〜〜〜
10
11
12〜〜〜
13〜〜〜
14
15〜〜〜
16ぅぅういえぇえあ
17
18〜〜〜
19
20
21〜〜〜
22
23〜〜〜
24〜〜〜ぅぅういえぇえあ
25
26
27〜〜〜
28
29
30〜〜〜
31〜〜〜
32〜〜〜ぅぅういえぇえあ
33〜〜〜
34〜〜〜
35〜〜〜
36〜〜〜
37〜〜〜
38〜〜〜
39〜〜〜
40ぅぅういえぇえあ


BUILDARGSは特別なメソッドで、Mooseによるインスタンス生成の途中で呼ばれるメソッドです。ここで適切な初期値を設定します。

インスタンス生成(new)の後で初期値を設定しようとすると、Roleから「○○の属性が無い」と言って怒られてしまいます。それで、生成が完了する前に、属性値を設定する必要があります。

無駄に長くなっているだけのように思われるかもしれませんが、本体のクラスとRoleを別の人が作成していると考えてみてください。両者に行き違いがあると、動きだす前に、Mooseがチェックしてくれます。

特に、requiresで要求したメソッドが無い時は、コンパイル時にチェックされます。たとえば、Nabeatuクラスの

with qw(Nabeatu::Role::Aho Nabeatu::Role::Ecstasy Nabeatu::Role::Conditions) ;

を次のように変更するとエラーになります。

with qw(Nabeatu::Role::Aho Nabeatu::Role::Ecstasy) ;

このエラーは次のように表示されます。

'Nabeatu::Role::Aho|Nabeatu::Role::Ecstasy' requires the methods 'aho_condition' and 'ecstasy_condition' to be implemented by 'Nabeatu' at /usr/local/share/perl/5.10.0/Moose/Meta/Role/Application.pm line 59
        Moose::Meta::Role::Application::apply('Moose::Meta::Role::Application::ToClass=HASH(0x8bfda68)', 'Moose::Meta::Role::Composite=HASH(0x8bfda38)', 'Moose::Meta::Class=HASH(0x8bb7158)') called at /usr/local/share/perl/5.10.0/Moose/Meta/Role/Application/ToClass.pm line 18
        (中略)
        Moose::with('Nabeatu::Role::Aho', 'Nabeatu::Role::Ecstasy') called at moose_nabeatu.pl line 79

「'aho_condition' と 'ecstasy_condition'という二つのメソッドが実装されてない」と言っていますが、重要なことは、この最後の 「line 79」 が、今、変更した withの行を指していることです。

このエラーは、print_numberが呼ばれる前、Nabeatuのインスタンスが生成される前に検出され、そこでプログラムの実行が止まるのです。

ということは、Nabeatu->new が実行された時点で、Nabeatuが使用している 3つのRoleが要求している条件は(Mooseで記述可能できちんと書かれている限りは)全て満たされていることが保証されるわけです。

多人数で大規模なプログラムを開発したり、オープンソースのモジュールを多く使うような場合は、全部組み合わせた時に、整合性が取れてないという問題が起こりがちです。Perlのように動的な言語では、特にそれが問題になりますが、Mooseを使うと、静的型言語に近いレベルで、そういう問題をチェックできます。

処理系に全ソースを読ませた時点でかなりのことがわかり、必要なインスタンスを生成した時点でほとんどのエラーがチェックされます。

それともう一つ面白いことは、Role を個別のインスタンスに対して動的にアサインできることです。

たとえば、アホになったりしない真面目なナベアツを作ってみます。

package Nabeatu::Dynamic ;
use Moose; 
with qw(Nabeatu::Role::Conditions) ;

has 'aho_string' => (
    is => 'rw' ,
    isa => 'Str',
    ) ;

has 'ecstasy_string' => (
    is => 'rw' ,
    isa => 'Str',
    ) ;

sub BUILDARGS {
    my ($class, %args) = @_ ;

    $args{aho_number} ||= 3 ;
    $args{ecstasy_number} ||= 8 ;

    return $class->SUPER::BUILDARGS(%args) ;
}

sub print_number {
    my ($self, $n) = @_ ;

    print $n ;
}

my $nabeatu_d = Nabeatu::Dynamic->new ;
$nabeatu_d->print_number(32) ; # ただの 「32」

これは、「〜〜〜」とか「ぅぅういえぇえあ」とか、ふざけたことを言わずに、数字だけを表示する真面目なナベアツです。

そして、このナベアツに Aho の Roleをアサインします。

$nabeatu_d->aho_string('〜〜〜') ;
Nabeatu::Role::Aho->meta->apply($nabeatu_d) ;

$nabeatu_d->print_number(32) ; # 32〜〜〜

さらに、EcstasyのRoleをアサインします。

$nabeatu_d->ecstasy_string('ぅぅういえぇえあ') ;
Nabeatu::Role::Ecstasy->meta->apply($nabeatu_d) ;

$nabeatu_d->print_number(32) ; # 32〜〜〜ぅぅういえぇえあ

つまり、このナベアツは、インスタンスごとに違うふるまいをさせることができるわけです。Aho抜きで「32ぅぅういえぇえあ」と言うナベアツを作ることもできます。

これは、JavaScriptのような Prototypeベースのプログラミングに近いと思いますが、この場合でも、Roleが要求している条件のチェックは行なわれます。

Roleが必要としているメソッドや属性が無いと、applyの時点でエラーになります。JavaScriptで同様のことを行なった場合には、(よほど工夫しないと)実際にprint_numberが実行されるまで、その不整合は検出されないと思います。

Mooseは、普通のクラスベースの使い方もできるし、「ポストモダン」なRoleによって、継承の木構造を浅くする(無くする)ような設計もできるし、プロトタイプベースで動的にふるまいを変えるようなインスタンスもできます。AOPっぽい機能についても他にもいろいろあるようです。

Mooseは、この本の理論的枠組みを元にしているそうです。

The Art of the Metaobject Protocol (The MIT Press)
The Art of the Metaobject Protocol (The MIT Press)

この考え方は、CLOSの基盤にもなっていて、かなり歴史のあるものです。

こういうメタプログラミングで言語を拡張してどんどん便利にしていこうという試みは、他にも多く行なわれていますが、理論的基盤がしっかりしてないと、メタな機能をあれこれ使った時に矛盾が生じて破綻していまいます。Moose はそういう意味でも安心して使える感じがしました。

それと、、Mooseとは関係ないですが、Perlの練習としてついでに、関数的なナベアツも書いてみました。こちらはあんまりうまく行かなかったけど、おまけとして付けておきます。

use strict;
use utf8;

my $3の倍数の時 = の倍数の時(3) ;
my $3がつく数の時 = がつく数の時(3);
my $8の倍数の時 = の倍数の時(8);
my $あほ =  の時だけ(または($3の倍数の時,  $3がつく数の時), "〜〜〜");
my $気持ちいい = の時だけ($8の倍数の時, "ぅぅういえぇえあ");

my $数字をそのまま = sub  { shift };
my $改行 = sub  { "\n" };

print map {
    my $n = $_ ;
    map {
        $_->($n) # 一つの数字に4つの関数を順番に適用して配列を作る
    } (
        $数字をそのまま,
        $あほ,
        $気持ちいい,
        $改行
    ) 
} 1..40; # ...という処理を1から40まで繰り返して配列の配列を作る

sub 倍数の時 { 
    my ($n) = @_ ;
    return sub {  
        my ($i) = @_ ;
        return $i%$n == 0;
    }
};

sub つく数の時 { 
    my ($n) = @_ ;
    return sub {  
        my ($i) = @_ ;
        return $i =~ /$n/ ;
    }
};

sub たは {
    my ($a, $b) = @_ ;
    return sub {
        my ($i) = @_ ;
        $a->($i) or $b->($i) ;
    }
};

sub 時だけ {
    my ($condfunc, $text) = @_ ;
    return sub {
        $condfunc->(shift) ? $text : "" ;
    }
}