Perl Hackers Hub

第35回Perlによる内部DSLの作り方(3)

(1)こちら⁠2)こちらから。

Plack::Builder から見る内部DSLを作るテクニック

それでは、Plack::Builderのコードを通して、Perlで内部DSLを作るうえでのテクニックを紹介していきます。Plack::BuilderはPlackディストリビューションの一部で、PSGIPerl Web Server Gateway Interfaceアプリケーションを定義するためのDSLが書かれています。

PSGIは、いまやPerl界のデファクトスタンダードとなったWebサーバとWebアプリケーションフレームワークをつなぐ仕様であり、Plackはその実装です。このように大成功したPSGI/Plackの中でDSLがまさに効果的に使用されています。ぜひ、この活きた例からDSLの使いどころとそれを実現するためのテクニックを習得してください。

Plack::Builderの使い方

まずPlack::Builderの使い方を見てみます。

app.psgi
use Plack::Builder;
my $app1 = sub { ... };
my $app2 = sub { ... };

builder {
  enable 'ReverseProxy';
  enable 'Runtime';
  mount '/foo' => $app1;
  mount '/' => $app2;
};

builderおよびその内部がPSGIアプリケーションを定義するDSLになっています。enableでPlack::Middlewareを有効にし、mountでURLをマップしています。あまりPSGIに馴染みがない人でも、なんとなくわかるのではないでしょうか。

Plack::Builderのコード

このDSLを実現しているPlack::Builderのコードがリスト1です。⁠これぞPerl」といったコードです。

リスト1は紙幅の都合で一部のみであり、いくつかの部分を改変しているため、ぜひ次のコマンドで実際のコードを確認してみてください。

$ perldoc -m Plack::Builder
リスト1 Plack::Buildeのコード
our $_add = our $_add_if = our $_mount = sub {  ―(1)
    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(&) {  ―(2)
    my $block = shift;

    my $self = __PACKAGE__->new;  ―(3)

    my $mount_is_called;
    my $urlmap = Plack::App::URLMap->new;
    local $_mount = sub {
        $mount_is_called++;
        $urlmap->map(@_);
        $urlmap;
    };
    local $_add = sub {  ―(4)
        $self->add_middleware(@_);
    };
    local $_add_if = sub {
        $self->add_middleware_if(@_);
    };

    my $app = $block->();  ―(5)

    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なしですっきりと書くことができます。

builder {
  ...;
};

もしプロトタイプ宣言なしにこう書いてしまうと、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のテストモジュールの使い方・作り方」です。お楽しみに。

おすすめ記事

記事・ニュース一覧