Perl の Mixin / Catalyst のプラグインの仕組み

何とタイムリーな記事。まつもとさんが「仕様の継承」と「実装の継承」について語っているので勝手にまとめてみる。

まつもとさんがITProに書いた記事がきっかけになったのか、最近 Mixin に関する話題をよく目にする、気がします。

Perl は多重継承をサポートする一方で、Exporter を使った Mixin も可能です。この辺はmiyagawa さんのメールマガジンのバックナンバーに詳しく書いてあります。

Exporter で関数を export しつつ、それを import した側で、関数ではなくインスタンスメソッドとしてそれを利用するというトリックを使うことで Mixin ができる、というものです。ただ、中でも触れられているように $self の扱いで注意になるケースがあります。

ところで、Perl on Rails なフレームワークの Catalyst のプラグインの仕組みを見てると興味深いコードが出てきます。

例えば Catalyst に Data::FormValidator を組み込むための Catalyst::Plugin::FormValidator の実装は

package Catalyst::Plugin::FormValidator;

use strict;
use NEXT;
use Data::FormValidator;

our $VERSION = '0.02';

sub form {
    my $c = shift;
    if ( $_[0] ) {
        my $form = $_[1] ? {@_} : $_[0];
        $c->{form} =
          Data::FormValidator->check( $c->request->parameters, $form );
    }
    return $c->{form};
}

1;

とこんな感じ。特に何かを継承しているわけではありません。(NEXT、なんていうメソッドの redispatch を実現するモジュール?が出てきてますが、その辺もプラグインアーキテクチャには関係してません。 今回の話とは関係ありません。)

ですが、Catalyst を使うときに

use Catalyst qw (/FormValidator/)

としてやると、Controller の中とかで

sub default : Private {
    my ( $self, $c ) = @_;
    my $result = $c->form(
        required => [qw(name title)],
    );
    if ($result->has_invalid or $result->has_missing) {
        ...
}

といった感じで、コンテキストオブジェクト ($c) で、Catalyst::Plugin::FormValidator で定義してある form メソッドが使えるようになります。コンテキストオブジェクトというのは Catalyst のコアみたいなもんで、こいつからクエリを取得したりテンプレートをセットしたりということができます。

明示的な継承を使っていないクラスを書くだけで、コアが拡張されるようになっている、というところがポイントです。この振る舞いは Mixin に近いものがあります。

このカラクリは Catalyst::Setup の setup_plugins メソッドにあって、以下のようなコードになってます。($plugins には use Catalyst で指定したプラグイン名が渡ります。)

sub setup_plugins {
    my ( $class, $plugins ) = @_;
    for my $plugin ( @$plugins ) {
        $plugin = "Catalyst::Plugin::$plugin";
        $plugin->require;
        if ( $@ ) {
            Catalyst::Exception->throw(
                message => qq/Couldn't load plugin "$plugin", "$@"/
            );
        }
        {
            no strict 'refs';
            push @{"$class\::ISA"}, $plugin;
        }
    }
}

Catalyst のコアはこの Catalyst::Setup を継承しており(この辺の継承関係は結構トリックが多用されているので説明するのがちょっと難しい)、その Catalyst::Setup の継承ツリーに対して

no strict 'refs';
push @{"$class\::ISA"}, $plugin;

とプラグインのクラス名を差し込む、ということをしてます。これにより、Plugin ディレクトリにプラグインのクラスを追加するだけで、特にそのプラグインクラスが何かを継承したり、コンストラクタを定義したりしなくても、コアを拡張することができるようになってます。

実際には継承ツリーの途中 (@Catalyst::Setup::ISA) に push したりしてるので多重継承、ということになるのでしょうがプラグインを開発する側から見ると Mixin に近い扱いになっています。またプラグインのクラスは実際に継承ツリーの中に入るため、$self 問題はおきません。なかなか面白い仕組みです。

動的な言語の強みを生かした実装、という感じですね。この仕組みが面白かったので、はてなフレームワークにも同様のプラグインロジックを組み込んでみました。