といったコードです。
リスト1は紙幅の都合で一部のみであり、いくつかの部分を改変しているため、ぜひ次のコマンドで実際のコードを確認してみてください。
$ perldoc -m Plack::Builder
リスト1 Plack::Buildeのコード
our $_add = our $_add_if = our $_mount = sub {
Carp::croak("enable/mount should be called "
. "inside builder {} block");
};
sub enable { $_add->(@_) }
sub enable_if(&$@) { $_add_if->(@_) }
sub mount {
my $self = shift;
if (Scalar::Util::blessed($self)) {
$self->_mount(@_);
}else{
$_mount->($self, @_);
}
}
sub builder(&) {
my $block = shift;
my $self = __PACKAGE__->new;
my $mount_is_called;
my $urlmap = Plack::App::URLMap->new;
local $_mount = sub {
$mount_is_called++;
$urlmap->map(@_);
$urlmap;
};
local $_add = sub {
$self->add_middleware(@_);
};
local $_add_if = sub {
$self->add_middleware_if(@_);
};
my $app = $block->();
if ($mount_is_called) {
if ($app ne $urlmap) {
Carp::carp("WARNING: You used mount() ...");
} else {
$app = $app->to_app;
}
}
$app = $app->to_app
if $app
and Scalar::Util::blessed($app)
and $app->can('to_app');
$self->to_app($app);
}
※ 誌面の都合上、一部のみでありかつ改変済み
内部DSLを作るうえでのテクニック
では、Plack::Builderのコード、特にbuilder
サブルーチンを読み解きながら、内部DSLを作るうえでのテクニックを見ていきましょう。
プロトタイプ──構文解析のヒントを与える
リスト1(2) を見てください。ここでbuilder
サブルーチンは後ろに(&)
を引き連れて定義されています。これはプロトタイプ宣言です。
プロトタイプとはサブルーチンの取り得る引数の型のことで、sub builder(&)
によって「builder
サブルーチンはサブルーチンリファレンスを引数に取る」と宣言しています。こうすると、Perlはその情報をもとに構文解析するため、引数のサブルーチンリファレンスをsub
なしですっきりと書くことができます。
もしプロトタイプ宣言なしにこう書いてしまうと、Perlは{}
を無名ハッシュと解釈してしまいシンタックスエラーとなります。
このようにDSL用途では、Perlに構文解析のヒントを与えるという目的でプロトタイプ宣言がよく使われます。
ところで『Perlベストプラクティス』( 注2 )では、プロトタイプの使用は避けるべきだと書かれています。実際、プロトタイプはサブルーチンの使用方法によってその効果が発揮されないときがあるため、バリデーション用途にはお勧めできません。一方で今回のようにPerlに構文解析のヒントを与えるという意味においては非常に有用なものとなります。
コンテキストが重要
さて次はリスト1(3) を見てください。ここで$self
にPlack::Builderインスタンスを設定しています。すなわち「主題」を設定しているのです。
ここで先のMojoliciousの例を振り返ってみます。オブジェクト指向版では、ルートオブジェクト$routes
にルーティングを$routes->get('/')...
のように定義していきました。一方、DSL版ではルートオブジェクトは表向きには示されておらず、単にget '/' =>...
と定義していきました。しかし、DSL版にも明らかに何らかのルートオブジェクトが仮定されており、そのルートオブジェクトのルーティングとして定義されていっています。
この例からわかるように、DSLでは何を主題としているか、どんな状況にあるかなどのコンテキストが明示されないことが多くあります。よってDSLを作っていく際には、「 主題を設定する」「 状況によって作用を変える」などをDSL作者が適切に行う必要があります。
builder
サブルーチンにおいては、これからPSGIアプリケーションを定義していくうえでの主題となるPlack::Builderインスタンスが設定され、以後それに対して操作をしていくのです。
local──あるスコープだけ意味を変える
続いてはリスト1(4) のlocal
です。local
を使うと、それが宣言されたスコープのみ変数やサブルーチンの意味を変えることができます。
身近な例としては、ファイルを一気に読み込む際、改行文字を表す$/
をundef
にすることがあります。
my $content = do {
open my $fh, "<", $file or die "$file: $!";
local $/ = undef;
<$fh>;
};
リスト1(1) にて、$_add
はもともと例外を出すだけのサブルーチンリファレンスとして定義されていました。それが主題$self
が設定されたbuilder
サブルーチン内でだけは、local $_add = sub {$self->add_middleware(@_)}
とMiddlewareを適用する役割を与えられるのです。
こうしてbuilder
サブルーチン内では主題$self
が設定され、その主題に合わせて$_add
の定義が変更されました。そして満を持してリスト1(5) にて$block
が実行され、PSGIアプリケーションが定義されます。
シンボルテーブル ── 動的にシンボルを定義する
さてPlack::Builderのコードには出てきませんでしたが、最後にシンボルテーブルについても触れます。DSLを作る際には実行時にサブルーチンを定義する必要が出てきます。Perlではシンボルテーブルを操作することでこれを実現できます。
シンボルテーブルとは、各パッケージの識別子とそれに対応する値たちが収められているテーブルです。*Module::foo
でModuleパッケージのfoo識別子のシンボルテーブルにアクセスできます。たとえば、あるパッケージに動的にfooサブルーチンを定義する場合は次のようにします。
use strict;
{
no strict 'refs';
my $package = "YourApp";
*{$package . "::foo"} = sub { print "foo!" };
}
YourApp::foo(); # foo!
ここでno strict 'refs'
としている理由について説明します。もし$package
という変数を使わず、
*YourApp::foo = sub { print "foo!" };
と定義するのであればno strict 'refs'
は必要ありません。しかしDSLでの用途を考えれば、実行時に変わる可能性のある変数を用いて定義するのが普通でしょうから、スカラの値を変数の「名前」として使われることを許すno strict 'refs'
を指定する必要があります。
ただしno strict 'refs'
を使う場合は、与える影響を抑えるために、上記のコードのように必要最小限のスコープでno strict 'refs'
を宣言するようにしてください。
まとめ
以上、DSLとは何かを説明し、Plack::Builderのコードを通してPerlによる内部DSLの作り方を見てきました。いかがでしたでしょうか。もしかしたら今回扱ったテクニックはPerlの黒魔術と呼ばれる部分かもしれません。しかし、入念に考えられたDSLは簡潔でわかりやすく、そして何よりクールなものになります。ぜひ一度ご自身でも実装してみてください。
さて、次回の執筆者はMagnolia.Kさんで、テーマは「Perlのテストモジュールの使い方・作り方」です。お楽しみに。