bonar note

京都のエンジニア bonar の技術的なことや技術的でない日常のブログです。

モダンPerl入門でMooseに入門してみた

Perl界隈の期待の新刊「モダンPerl入門」。読んじゃいました。

モダンPerl入門 (CodeZine BOOKS)

モダンPerl入門 (CodeZine BOOKS)

Perl基礎の表層を一通りなめたものの、初心者からなかなか抜け出せない僕のような人のためにかかれた本ですね。感動しました。読んで満足してこのまま終わってしまいそうだったので、書いてある内容を実践してみてようと思います。

注意

  • 調べながら書きながらなので間違っている箇所もあるかもです。ツッコミお待ちしております。
  • Moose と Class::MOP の機能をおそらく混同してます。すいません。

Mooooooose

モダンPerl入門は "Class::Accessor::Fast と Moose" という話題から始まります。Moose に関してはこのまえのYAPCで初めてして、「へー何か良くわからないけどすごいんだろうな」程度にしか思っていなかったので、こんな風に書けるんだっていうのがわかって勉強になりました。

今書いている Music::AutoPhrase という自動作曲ライブラリをbranch切って修正してみました。元々 Class::Accessor::Fast を使って書かれていて、設計や実装に関しても甘いところが多く、題材としては微妙かもですがご容赦を。

例えば以下は Music::AutoPhrase::Code というコード(例:Cm7, F, C7)を表現するクラスはこんな風に変えました。

使用前
http://coderepos.org/share/browser/lang/perl/Music-AutoPhrase/trunk/lib/Music/AutoPhrase/Code.pm

使用後
http://coderepos.org/share/browser/lang/perl/Music-AutoPhrase/branches/moose/lib/Music/AutoPhrase/Code.pm

Exportやめたりとかの関係ない修正も入っているのでちょっと比べずらいかもです。。

has

Class::Accessor とくらべて見た目の一番の違いはhasでしょうか。そのattributeは一体どんなものでどんなデータが入るのかっていうのが細かく指定できて、且つそれがソースの先頭に列挙されるので、かなり見通しが良くなる気がしますね。

has string_value => (
    is       => 'ro',
    isa      => 'Str',
    required => 1,
);

値が必須かどうかや、読み書き制限等便利なフラグが盛りだくさんですが(perldoc Moose をみるとhasはすごい数の設定項目が)、特に値の型を指定できる isa が便利だと思いました。Str, Int, ArrayRef みたいな単純なものだけでなく、ArrayRef[Int] のように組み合わせて使う事もでき、この[Int]の部分は自分で作ったpackageを入れることも出来ます(これが最高に便利)。

has candidate => (
    is       => 'ro',
    isa      => 'ArrayRef[Int]',
    required => 1,
);

型の種類については perldoc Moose::Util::TypeConstraints を参照。

subtype

isa では上記のような既存の型以外に自分で作った制約を当てることもできます。例えば、Cm7 というコードを C(Note Main) + m7(Note Sub) に分けて保持する場合、それぞれの
パーツで許容できる文字列に制限をかけたかったりします

  5 use Moose::Util::TypeConstraints;
  6 
  7 subtype 'NoteMain'                                                                                                                                
  8     => as 'Str'
  9     => where { defined $_ && $_ =~ /^(C|D|E|F|G|A|B|X)(#|\-)?$/ }
 10     ;
 11 subtype 'NoteSub'
 12     => as 'Str'
 13     => where { !defined $_ || $_ =~ /^(m|m7|M7|7|X)$/ }
 14     ;

 21 has note_main => (
 22     is       => 'ro',
 23     isa      => 'NoteMain',
 24     required => 1,                                                                                                                                
 25 );
 26 has note_sub => (
 27     is       => 'ro',
 28     isa      => 'NoteSub',
 29     required => 1,
 30 );
new() and BUILDARGS()

Moose なクラスでは自分でnew()を書く事は無く、Moose::Object のnew() が使われます。上記の Music::AutoPhrase::Code の場合だと以下のようにhasで作った属性にhashで値を渡します。

my $code = Music::AutoPhrase::Code->new(
    string_value => 'Cm7',
    note_main    => 'C',
    note_sub     => 'm7',
);

ただ、実際にはこれでは不便で、string_value を文字列で指定して、渡した文字列を勝手に切り分けて欲しいかったりする時もあります。こんな感じで

my $code = Music::AutoPhrase::Code->new('Cm7');

そんな時にnew()が書けないのが不便だなと思ったのですが、その辺は考えられていて、BUILDARGS() という関数を定義すると、それをnew()に渡すhashの整形処理として使ってくれます。

new()の引数を受け取って、Moose::Object->new() に渡すhashのリファレンスを返すだけです。今回の例だとこんな感じになりました。
#_parse_code() が文字列を切り分ける関数

sub BUILDARGS {
    my ($class, $string_value) = @_;
    return if !defined $string_value;

    my @parsed = _parse_code($string_value);
    return if !@parsed;
    my ($candidate, $note_main, $note_sub) = @parsed;

    return {
        note_main    => $note_main,
        note_sub     => $note_sub,
        candidate    => $candidate,
        string_value => $string_value,
    };

さらにオブジェクトを作成したあとの初期化処理もBUILD()という関数を定義することで作成できます。Moose::Object の BUILDALL() という関数が継承ツリーの全てのBUILD()を実行します。

coercing

coerce もMooseの特徴的な機能ですね。僕が知らないだけで特に特徴的で無かったらすいません。

Mooseなクラスでは属性値の型を定義できて、違う型が与えられた場合、元の型が変換可能なものであれば自動で変換するという機能です。

今回の例でいうと、Music::AutoPhrase::Channel というクラスがあり、これは一つの楽曲内のそれぞれの楽器チャンネルを表現しています。その中に octav という属性があります。これはそのチャンネルのオクターブ(音の高さ)のレンジを規定しています。

has octav => (
    is       => 'rw',
    isa      => 'OctavList',
    default  => sub { [qw/4 5 6/] },
    required => 1,
    coerce   => 1,
);

これで、

my $channel = Music::AutoPhrase::Channel->new(%arg);
$channel->octav([qw/3 4 5 6/]);

みたいにして書くわけですが、与えるarrayrefの要素数が1個だった場合に、$channel->octav([4]); ではなく $channel->octav(4) という書き方も許容したくなります。そういった場合には、coerce で特定のsubtype に対する変換処理をあらかじめ書いておく事が出来ます。
#coerce を有効にするには has で coerce => 1 を指定

subtype 'OctavList'
    => as 'ArrayRef'
    => where { is_valid_octav($_) or return for @$_; 1; };
coerce 'OctavList'
    => from 'Int' => via { [$_] };

これで「Intが渡された時にはその値1つだけを要素としてもつarrayrefに変換」というルールを教えることが出来ます。こうしておくと、以下のような全く関係ない値を与えると、

my $channel = Music::AutoPhrase::Channel->new();
$channel->octav('a');

不正な値としてエラーになりますが、

Attribute (octav) does not pass the type constraint because: Validation failed for 'OctavList' failed with value a at foo.pl line 6

数値を与えるとちゃんと変換してくれます。

use Data::Dumper;

my $channel = Music::AutoPhrase::Channel->new();
$channel->octav(4);
warn Dumper $channel->octav;
$VAR1 = [
          4
        ];

非常に面白い仕組みで便利なのですね。あと、coerce をなんと発音するのか実はわからないので(こあーす?)誰か教えてください。

MooseX::AttributeHelpers

モダンPerl入門ではデザインパターンの章で紹介される MooseX::AttributeHelpers ですが、個人的にはこれはかなり衝撃的でした。このモジュールを使うと、hasでその属性のmetaclassを指定した際に、その属性に関する操作を自動生成することが出来ます。

例えば先ほどの Music::AutoPhrase::Channel では、beats という属性があります。

has beats => (
    is         => 'rw',
    isa        => 'ArrayRef[Music::AutoPhrase::BeatPattern]',
    default    => sub { [] },
    required   => 1,
    }
);

これはちょっと説明しづらいのですが、、そのチャンネルにおける音符配置情報で、どういうタイミングで音が鳴るのかという情報の配列になっています。データとしては isa で表現されている様に Music::AutoPhrase::BeatPatternオブジェクトの配列(のリファレンス)になります。

こういう属性があると以下のようなアクセサを書きたくなります。内部で持っている arrayref に push したり、中身を全部消したり、といった操作です。

sub push_beat { 
    my ($self, $new_beat) = @_;
    my $beats = $self->beats;
    push @$beats, $new_beat;
    $self->beats($beats);
}

sub clear_beat {
    my ($self) = @_;
    $self->beats([]);
}

こういったものも、metaclass と providers(メソッドマッピング)を指定することで簡単に作成できます。

use MooseX::AttributeHelpers;

has beats => (
    is         => 'rw',
    isa        => 'ArrayRef[Music::AutoPhrase::BeatPattern]',
    default    => sub { [] },
    required   => 1,
    metaclass => 'Collection::Array',
    provides  => {
        push  => 'push_beat',
        clear => 'clear_beat',
    }
);

これで、$channel->push_beat($beat) みたいに使えるようになります。すばらしいですね。どういったメソッドが使えるかについては

perldoc MooseX::AttributeHelpers::MethodProvider::Array
perldoc MooseX::AttributeHelpers::MethodProvider::List
perldoc MooseX::AttributeHelpers::MethodProvider::Hash
perldoc MooseX::AttributeHelpers::Number
perldoc MooseX::AttributeHelpers::Counter
perldoc MooseX::AttributeHelpers::Bool

に書いてあります。

Role

Music::AutoPhrase::Channel には selector という属性があります。これは、自動作曲する際にどういう基準で音を選ぶのかというロジックを入れる場所です。内部状態の遷移によって出力がかわる可能性があるので関数リファレンスではなくオブジェクトをセットする必要があり、且つそのオブジェクトは select_note() という音を選び出すメソッドを持っている必要があります。
#すいません、意味不明ですよね。。

いままでは以下のようなコードで該当するメソッドがあるかを確認していました。

    # set custom selector
    if (defined $arg{selector}) {
        my $fullns = SELECTOR_NS_PREFIX . $arg{selector};
        if ($fullns->require() && $fullns->can('select_note')) {
            $self->selector($fullns->new());
        }
    }

Moose では with 'ロール名' という宣言で、あるクラスがあるメソッドを持っていることを保証できます。まずは、

lib/Music/AutoPhrase/NoteSelector.pm

package Music::AutoPhrase::Role::NoteSelector;
use Moose::Role;

requires 'select_note';

1;

ロールの制約を受ける側ではwithで上記のロールを指定するだけです。

lib/Music/AutoPhrase/NoteSelector/Simple.pm

package Music::AutoPhrase::NoteSelector::Simple;

use Moose;
use Music::AutoPhrase::Note;
use Music::AutoPhrase::NoteSelector;
use List::Util qw/shuffle/;

extends 'Music::AutoPhrase::NoteSelector';    # 継承
with 'Music::AutoPhrase::Role::NoteSelector'; # ロール

__PACKAGE__->meta->make_immutable;
no Moose;

この後に実際は select_note() の定義があるのですが、この指定したメソッドが無いと use した時点でエラーが出るようになります。

'Music::AutoPhrase::Role::NoteSelector' requires the method 'select_note' to be implemented by 'Music::AutoPhrase::NoteSelector::Simple' at /Library/Perl/5.8.8/Moose/Meta/Role/Application.pm line 59

その後この NoteSelector を継承した NoteSelector::Simple のオブジェクトを格納する Music::AutoPhrase::Channel 側では、has のオプション "does" を使って、格納されるオブジェクトが上記のロールを持っていなくてはならないという制約を追加します。

has selector => (
    is      => 'rw',
    does    => 'Music::AutoPhrase::Role::NoteSelector',
    default => sub { Music::AutoPhrase::NoteSelector::Simple->new(); },
);

なんというか、こうちゃんととやってる感じが出ますね。new() のなかで can() で調べるとかよりはずっと見やすいなと思いました。

まとめ

そんなこんなで本を読み進めながら branches/moose を修正していったのですが、Moose化だけにして差分を奇麗に見せたいなと思っていたのですが、やりだしたら次から次へとバグが見つかってしまって、こらえきれずに色々直してしまいました。
branch:http://coderepos.org/share/browser/lang/perl/Music-AutoPhrase/branches/moose

Mooseの機能の1%も使えてないですが、その前提で感想を述べさせていただきますと、「書いた人の意図がわかりやすい、それがいい」って感じなのかなと思いました。何かのモデルクラスの場合、ソースの先頭で属性の一覧があり、そこに何が入るのか、デフォルトは何か、必須項目か、みたいな全体像が一気に見えるので、把握しやすくなりますね。

また、subtype/isa による値チェックやcoerce、provides によって、ほかの部分に散らばっていた雑多な処理がまとまってすっきりする気がします。属性値の設定とそのチェックロジックが近くにあるっていうのもいいですね。書き方の問題ですが。

モダンPerl入門は、これらのトピック以外にも テスト/ベンチマーク/XS など盛りだくさんの内容でどれもとても実践的です。ほかのトピックに関しても色々やってみたいですね。
なんかあんまり本の紹介になってないですが、全てのPerlプログラマにオススメできる良書です。まだの方は是非。