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

第10回Class::Meta::Express:もっと読みやすく、周囲への影響は最小限に

「シュガー関数=モダン」ではありませんが

「モダンPerlがわからない」と言われる大きな原因のひとつが、MooseJiftyに見られるシュガー関数、ドメイン特化言語(DSL)の氾濫にあることは衆目の一致するところでしょう。

前回紹介したJiftyでは、スキーマとアクション用にそれぞれひとつ、ディスパッチャ用にひとつ、テンプレート用にひとつ、という具合に都合3系統4種類のドメイン特化言語が使われていましたし、Mooseの場合も、アトリビュートや型の定義にメソッドモディファイアと、さまざまなところで独自の記法が用意されています。Catalystも、テストの際には独自のシュガー関数を使っていました。もちろん探せば似たような例はいくらでも見つかることでしょう。

このようなシュガー関数は、1998年にリリースされたPerl 5.5でコアに導入されたTestモジュールなどを見てもわかる通り、本来はモダンPerlに特有のものではありません。オブジェクト指向的な書き方が広まった影響でひと頃不遇をかこっていたのが、近年、あらためてその簡潔さが見直されるようになって、まるでモダンPerlの代名詞的な扱いをされるようになったものです。

もっとも、昔のシュガー関数と、いまのシュガー関数では、たしかに様子が異なる部分もあります。今回は、そのような違いに注目しながら、シュガー関数と呼ばれるもの、とりわけクラスやオブジェクトの設定に寄与しているものの裏側を少し覗いてみることにしましょう。

設定をまるごと放り込むタイプ

設定を行うためのシュガー関数としては、15年前、Perl 5の登場とともにうまれたExtUtils::MakeMakerのWriteMakefile(当時の表記にしたがえばwriteMakefile)あたりが最古層といってよいでしょう。

これも細かなところは時代とともにいろいろ変わりましたが、必要な情報をハッシュとして渡すと、内部で適切な変換を行ってMakefileを作成し、⁠ここでは利用していませんが)設定を格納したオブジェクトを返す、という基本は変わっていません。

use strict;
use warnings;
use ExtUtils::MakeMaker;

WriteMakefile(
    NAME         => 'Foo',
    VERSION_FROM => 'lib/Foo.pm',
    PREREQ_PM    => {
        'Some::Module' => '0.01',
    },
);

呼び出し元のクラスに設定が反映されるわけではない、という意味ではMooseやJiftyのシュガー関数とは趣が異なりますが、必要な情報のみ宣言すればあとはすべてモジュールのほうで良きに計らってくれる(内部のアルゴリズムを気にする必要はない)という点で、このWriteMakefile(や、それを利用するMakefile.PL)のインタフェースは非常に宣言的です。

また、このような仕組みは、モジュール内部の処理が変わってもインタフェース部分が変わらなければそのまま使い続けられるという意味で、非常に後方互換性を維持しやすいものであるともいえます。

複雑な設定に対応するのは大変

その一方で、この方式は、インタフェース部分しかユーザに開放されていない分、高度な処理を実行させようとすると設定が複雑なものになりがちなのがネックといえます。

たとえば、特定のOSの場合だけ依存モジュールを増やしたいとき、WriteMakefileの中にはしばしば三項演算子による条件分岐が入り込みます。

WriteMakefile(
    NAME         => 'Foo',
    VERSION_FROM => 'lib/Foo.pm',
    PREREQ_PM    => {
        'Some::Module' => '0.01',
        (($^O eq 'MSWin32')
            ? ('Win32::Module' => '0.01')
            : ()
        ),
    },
);

もちろんこれはもう少しベタに、このような書き方をしてもかまいません。

my %config = (
    NAME         => 'Foo',
    VERSION_FROM => 'lib/Foo.pm',
    PREREQ_PM    => {
        'Some::Module' => '0.01',
    },
);

if ($^O eq 'MSWin32') {
    $config->{PREREQ_PM}->{'Win32::Module'} = '0.01';
}

WriteMakefile(%config);

ただ、いずれにしてもこのようなやり方では、設定が複雑になってくるとインデントが深くなったり、かっこの数が増えて一覧性や可読性が悪くなりますし、場合によってはハッシュや配列の合成を行う余分なコードを書かなければならなくなってしまいます。

また、このようにハッシュを多用する設定の場合、定義ミスを見落としやすいのも欠点のひとつといってよいでしょう。内部的に必須属性のチェックを行っていればよいのですが、PREREQ_PMのように、かならずしも存在するとは限らないものの場合、つづりを間違えて「PREREQ」と書いてあっても、内部的にはふつう「PREREQ_PMはない⁠⁠、と判断しておしまいになってしまいがちです。

設定を細分化して可読性を高めたタイプ

こういった問題に対するひとつの回答として登場したのが、2003年にリリースされたModule::Installです。

Module::Installを使うと、上の例はこのように書けます。

use strict;
use warnings;
use inc::Module::Install;

name 'Foo';
all_from 'lib/Foo.pm';
requires 'Some::Module'  => '0.01';
requires 'Win32::Module' => '0.01' if $^O eq 'MSWin32';

WriteAll;

ExtUtils::MakeMakerの例に比べると、細かな設定が1センテンスで指定できるようになったため、見た目はすっきりしました。また、今回はシュガー関数の名前を間違えると構文エラーになるのもポイントといってよいでしょう。

タイプ数については微妙なところですが、少なくともオブジェクト指向プログラミングで同等の処理を実現した場合に比べれば、目にも手にも、はるかにやさしいものになっていることは間違いありません。

Makefile.PLの中で使う分には問題ありませんが

もっとも、Module::Installの方式にも弱点はあります。

たとえば、Makefile.PLという、ふつうは1回しか実行されないスクリプトのなかではそれほど問題にはならないことですが、Module::Installは大量のシュガー関数をエクスポートしているので、インポート先のクラスでうっかり同名の関数/メソッドを定義してしまうと、期待した動作が得られないことがあります。

ことModule::Installについては、どのクラスでどのシュガー関数が定義されているかわかりづらい、という問題もあります。Module::Installは拡張性を重視してプラガブルな構成になっているのですが、シュガー関数の名前とクラスの名前はかならずしも一致しないので、慣れないと個々のシュガー関数のPODを見つけるのにも苦労します(これはModule::Installに限らず、継承関係が複雑なモジュールにはつきものの問題ですが、エクスポートされてくるシュガー関数は一見Perlの組み込み関数のようにも見えてしまうため、なおさら出自がわかりづらくなっています⁠⁠。

また、ここで宣言した設定は、内部的には$Module::Install::MAINというパッケージ変数に保存されているオブジェクトに格納されます。Module::Installの用途的にはこれで十分用事は足りていますが、Mooseのようにクラスを定義するようなモジュールで同じ手法を利用する場合は保存先を考え直す必要があるでしょう。

noによるキーワードの除去

そのMooseですが、これも大量のシュガー関数を使いますので、基本的にはModule::Installと同じ問題を抱えています。

ただし、Mooseの場合、自分がエクスポートしたシュガー関数については、no Mooseを実行すれば取り除くことができるようになっています。

これまではスペースの都合で省略してきましたが、一般的にMooseを使ったクラス定義では、それ以上Mooseのキーワードが必要なくなった時点で、下の例のようにno Moose;という行を入れておくのがベストプラクティス。こうしておくと、たとえばこのような行儀の悪いコードを書いても正しく動作するようになります。

package MyClass;

use Moose;

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

no Moose;

sub has { shift; print @_, "\n" }

package main;

my $obj = MyClass->new;
$obj->has('foo');  # foo
$obj->foo('bar');
print $obj->foo;   # bar

1;

範囲を明示的に指定してよいとこ取りをする

もっとも、この問題ではデイヴィッド・ホイーラー(David Wheeler)氏が2006年5月にリリースしたClass::Meta::Expressなどのほうが一歩先を進んでいます。

このClass::Meta::Expressは2004年にリリースされたClass::Metaという、Class::MOPの先輩格にあたるイントロスペクション用モジュールのラッパーなのですが、これを使うと、もともとはこのように冗長な書き方をしていたClass::Metaの定義が、

package Person;

use strict;
use warnings;
use Class::Meta;
use Class::Meta::Types::String;

BEGIN {
    my $meta = Class::Meta->new();

    $meta->add_constructor( name => 'new' );

    $meta->add_attribute(
        name     => 'name',
        is       => 'string',
        required => 1,
    );

    $meta->add_method(
        name => 'say_hello',
        code => sub {
            my $self = shift;
            print "Hello, I'm ", $self->name, "\n";
        },
    );

    $meta->build;
}

このように簡潔に書けるようになります。

package Person;

use strict;
use warnings;
use Class::Meta::Express;

BEGIN {
    class {
        ctor 'new';

        has name => (is => 'string', required => 1);

        method say_hello => sub {
            my $self = shift;
            print "Hello, I'm ", $self->name, "\n";
        };
    };
}

これだけならMooseと大差ないように見えますが(実際、この書き方はMooseをまねたものであることがPODに明記されています⁠⁠、Class::Meta::Expressの場合、宣言部をclassというブロック(に見える無名関数)内に押し込めることによって、クラスの定義が済んだら利用したシュガー関数を自動的に取り除いてくれる、というのが大きなポイント。

また、$meta->buildのようなお決まりの終了処理も(ExtUtils::MakeMakerのWriteMakefileのように)すべてclassのほうで処理してくれるので、よりタイプ数の少ない宣言が可能になっています。

また、当初はMooseやModule::Installのように宣言をベタ書きしていたものの、2007年初頭に後方互換性を捨ててまでこの書き方を採用したJifty::DBIでは、この宣言をuseと組み合わせることによって、明示的なBEGINブロックを不要にしています。

package MyApp::Table;

use strict;
use warnings;
use Jifty::DBI::Schema;
use Jifty::DBI::Record schema {
    column foo => type is 'text';
};

やりすぎにはご用心

このように、モダンPerlの世界では、シュガー関数ひとつとっても昔より気を遣った書き方がされるようになっているのですが、なにごとにも限度というものはあります。

たとえば、フロリアン・ラグヴィッツ(Florian Ragwitz)氏が2008年10月にリリースしたMooseX::Declareというモジュールを使うと、このような書き方さえできるようになるのですが、

use MooseX::Declare;

class BankAccount {
    has 'balance' => ( isa => 'Num', is => 'rw', default => 0 );

    method deposit (Num $amount) {
        $self->balance( $self->balance + $amount );
    }

    method withdraw (Num $amount) {
        my $current_balance = $self->balance();
        ( $current_balance >= $amount )
            || confess "Account overdrawn";
        $self->balance( $current_balance - $amount );
    }
}

これ、package BankAccount;のような定番のパッケージ宣言すら省略してしまうため、そのままではPAUSEやCPAN検索サイトなどにパッケージが存在しないものと誤解されてしまう、という問題が知られています(そのため、CPANにアップされているモジュールではわざわざ別にpackageを使ったパッケージ宣言が書かれています⁠⁠。

MooseX::Declareの場合、裏ではソースフィルタ的な処理も行われているので、ここで取り上げたシュガー関数と同列に扱うことはできませんが、このように目にも手にもやさしい書き方を追究していく過程では、小粒でもぴりりと辛いツールがいくつも書かれています。次回はそのような小物に少しスポットを当ててみましょう。

おすすめ記事

記事・ニュース一覧