Perlでプラガブルモジュールを作ろう!

第2回attributeを使ってplugin作成を支援する

今回のテーマ

今回は、Perlのattributeという仕組みの詳細と、そのattributeを利用したClass::Componentのpluginについての解説、pluginの作成方法といった話題を取り上げます。

サンプルアプリケーション

本連載では、プラガブルなモジュールを作製するという事を考えて、Gopperという実際に実行可能なサンプルアプリケーションを元に解説を行ないます。GopperはCodeRepos上のsvnリポジトリに置いてあるので各自checkoutしてください。

svn co -r 455 http://svn.coderepos.org/share/lang/perl/Gopper/trunk Gopper

attributeとは

attributeとは、Perlのサブルーチンや変数に属性を定義して、サブルーチンとしての挙動を標準から変更したり、どのようなサブルーチンなのかを定義付けする事が出来ます。

たとえばCatalystを利用してControllerを書くときには

sub auto : Private {
    my ( $self, $c ) = @_;

    ...

    }
/code>

などと書きますよね。この例ではPrivateの部分がattributeになります。

この説明だけでは、なかなかピンとこないので実例を混ぜつつattributeについて解説します。

Perl標準で利用できるattribute

たとえば標準ではlvalueという属性が利用できます。

package Jitensya;
use strict;
use warnings;

sub new { bless { sound => 'リンリン' }, shift }

sub sound : lvalue {
    shift->{sound};
}
1;

package main;
use strict;
use warnings;

my $mama = Jitensya->new;
print $mama->sound; # 「リンリン」と出力

$mama->sound = 'チリンチリン';
print $mama->sound; # 「チリンチリン」と出力


と記述でき、サブルーチンやメソッドを左辺値として扱うことが出来ます。

標準で利用できるattributeはlvalueの他に、thread処理で利用するmethod/locked/sharedと、ourで利用するunique(perldoc -f our)があります。

ただ、これだけではCatalystで利用するようなattributeは逆立ちしたって使えません。 どうするかというと、Package-specific Attribute Handling(package固有に属性をハンドリングさせる)という機能を利用します。

Package-specific Attribute Handling

自作モジュール中に独自のattributeを実装する事もできます。普通の状態では、先述の標準で組み込まれたattributes以外のattributeを指定するとエラーが発生してしまいます。

#example.pl
use strict;
use warnings;

sub foo : bar {
}
$ perl ./example.pl 
Invalid CODE attribute: bar at ./example.pl line 4
BEGIN failed--compilation aborted at ./example.pl line 5.

ではどうするのかというと、attributeを利用したいパッケージ内にてMODIFY_CODE_ATTRIBUTESというサブルーチンを定義する事により、独自のattributeを利用出来るようになります。

#example2.pl
use strict;
use warnings;

print "script start\n";

sub MODIFY_CODE_ATTRIBUTES {
    my ($pkg, $ref, @attrs) = @_;
    print "MODIFY_CODE_ATTRIBUTES: set up\n";
    print "MODIFY_CODE_ATTRIBUTES: attrs: $_\n" for @attrs;
    return;

}

sub test : catalyst sledge(miyagawa) soozy(yappo) boofy {
    print "test: start\n";
}

test;
$ perl ./example2.pl 
MODIFY_CODE_ATTRIBUTES: set up
MODIFY_CODE_ATTRIBUTES: attrs: catalyst
MODIFY_CODE_ATTRIBUTES: attrs: sledge(miyagawa)
MODIFY_CODE_ATTRIBUTES: attrs: soozy(yappo)
MODIFY_CODE_ATTRIBUTES: attrs: boofy
script start
test: start

このような形でattributeを活用する事が出来ます。

更に詳細の話は、次回に行う予定です。

余談になりますが、perldoc attributes の中の Package-specific Attribute Handlingに関するドキュメントには

WARNING: the mechanisms described here are still experimental.  Do not rely on the current implementation.  In particular, there
       is no provision for applying package attributes to ’cloned’ copies of subroutines used as closures.  (See "Making References" in
       perlref for information on closures.) Package-specific attribute handling may change incompatibly in a future release.


と記載されており、パッケージ独自のattributeに関しては、実験的な実装だと強調されています。しかし、 Catalyst や DBIx::Class などで多数のユーザーに利用されてしまっている機能なので、きっと気にしなくても大丈夫です(多分⁠⁠。

Attribute::Handlers

attributeを簡単に利用する為のCPANモジュールという物が存在しています。これは、attributeを定義する為に実装されている、Attribute::Handlersが提供するattributeを用いて、独自attributeを実装する事が出来るモジュールです。

これを利用したCPANモジュールも、いくつか存在しています。たとえばアノmiyagawaさん作のAttribute::Protectedというモジュールがあります。public/private/protectedというJavaの修飾子のような事を、パッケージ内のメソッドに適用する事が出来ます。

実際どのように使うかを、解説を交えたコードは下記の通りです。

#example3.pl
use strict;
use warnings;
use Attribute::Handlers;

sub sample : ATTR(CODE) { # attributeとして実装したいメソッドに ATTR(CODE) というattributeを指定する
    my($package,   # メソッドの属するパッケージ名
        $symbol,   # メソッドのシンボル (シンボルテーブルで、そのまま使える文字列)
        $referent, # メソッドのコードリファレンス
        $attr,     # attribute名
        $data,     # attributeに渡された引数
        $phase     # このsampleメソッドが実行されている、Perl上の実行フェーズ
    ) = @_; # ATTR(CODE) を指定したメソッドに対しては、上記の引数が渡される
    my $name = *{$symbol}{NAME};
    no warnings 'redefine';
    *{$symbol} = sub {
        warn "goto: $package->$name : $attr($data)";
        goto &$referent;
    };
}

sub geek : sample(hoge) {
    my $name = shift;
    print "$name geek\n";
}

geek('a');
geek('e');
geek('g');

$ perl ./example3.pl 
goto: main->geek : sample(hoge) at ./example3.pl line 10.
a geek
goto: main->geek : sample(hoge) at ./example3.pl line 10.
e geek
goto: main->geek : sample(hoge) at ./example3.pl line 10.
g geek

このように比較的簡単にattributeを実装できます。より詳細な内容はperldoc Attribute::Handlersを実行するか、CPANに登録されているAttribute::で始まるいくつかのモジュールを参考にして下さい。

Attributeを活用したPlugin作成術

このattributeをplugin的に利用してる例がDBIx::Class(以下DBIC)です。詳しくはDBIx::Class::ResultSetManagerというモジュールのドキュメントに書いてあるのですが、ResultSetというattributeを使用する事によりDBICのResultSetを拡張出来ます。

#DBIx::Class::ResultSetManagerのPODより
  # in a table class
  __PACKAGE__->load_components(qw/ResultSetManager Core/); # note order!

  # will be removed from the table class and inserted into a
  # table-specific resultset class
  sub search_by_year_desc : ResultSet {
    my $self = shift;
    my $cond = shift;
    my $attrs = shift || {};
    $attrs->{order_by} = 'year DESC';
    $self->search($cond, $attrs);
  }

  $rs = $schema->resultset('CD')->search_by_year_desc({ artist => 'Tool' });

ResultSetManagerを参考にして、別のattributeをハンドリングするDBICのcomponentを書けば、DBICを面白く拡張出来ると思われます。

どの辺りで実装されているかは、DBIx::ClassとDBIx::Class::ResultSetManagerのソースをご覧下さい。ただし、ご多分に漏れずEXPERIMENTAL(実験的な扱い)です。

Class::Componentでpluginを作る

Class::ComponentはDBICのResultSetManagerを参考にして、pluginを簡単に実装する為のattributesを提供しています。どのようにしてPluginを作成するのかを、以下にサンプルアプリケーションGopperのコードと説明を交えて解説していきます。

Gopper::Plugin以下にモジュールを追加する

pluginを作る為には、モジュール名::Plugin以下の名前空間にpluginを追加する必要があります。モジュール名というのは、use Class::Componentをしているモジュールの事を指します。Gopperの場合は、Gooper.pmにてuse Class::Componentしているので、Gopper::Plugin以下にpluginを追加する事になります。

package Gopper;

use strict;
use warnings;
our $VERSION = '0.01';

use Class::Component;
use base qw( Class::Accessor::Fast );
__PACKAGE__->mk_accessors( qw/ config engine / );

#以下略 

今回は例として、Gopperのconfigをダンプするメソッドを追加するpluginを作ります。 名前は、そうですねConfigDumpがいいでしょう。以下にConfigDumpのコードを記します。

package Gopper::Plugin::Method::ConfigDump;
# 別にGopper::Plugin::ConfigDumpでもよいんだけど
# Gopper的には Method::ConfigDump が望ましい
use strict;
use warnings;
use base 'Class::Component::Plugin';

use YAML;

sub config_dump : Method { # Gopperに config_dump メソッドが追加される
    my($self, # ConfigDump pluginのインスタンス
        $c    # Gopper のインスタンス
    ) = @_;
    $c->log(debug => $self->to_yaml($c));
}

sub to_yaml { # Gopper には影響しないメソッド
    my($self, $c) = @_;
    Dump $c->config;
}

1;

たったこれだけのコードでGooperに対してメソッドを追加するpluginが書けました。

まず、Class::Componentのpluginとして動作する為には、Class::Component::Pluginを継承していなければなりません。Class::Component::Pluginを組み込む事により、pluginとしての初期処理やattributeのハンドリングを自動的に行います。

その後はpluginとして実装したい事を自由に書きます。各pluginのモジュールはGopper本体や他のpluginと干渉しないオブジェクトになっているので、Catalystのplugin作成のように他のpluginとのメソッド名の衝突を恐れる必要はありません。Plaggerのpluginのノリで実装出来ちゃいます。

メソッドを生やす?

Gopperにメソッドを追加する為に肝心な事は、Gopperに生やしたいメソッドにはMethodというattributeを指定しなければなりません。Methodを指定されたメソッドは、そのメソッド名でGopperからcallする事が出来ます。

厳密にいうとメソッドが生えるわけではなく$gooper->call('config_dump')というcallメソッドから呼び出せるようになります。ただし Class::Component::Component::Autocall::* といったcomponentを利用する事により、本当にメソッドが生えるようになります。

package Foo;
use strict;
use warnings;
use base 'Class::Component::Plugin';
__PACKAGE__->load_components(qw/ Autocall::InjectMethod /);
1;

といったコードを書く事により、Method attributeで追加したメソッドを本当に生やす事が出来ます。

追加したモジュールを利用可能にする

ただpluginを書いただけではGopperに影響を与える事は出来ません。そこで追加したpluginを利用するコードを書く必要があります。

何をするかと言うと、追加したpluginをロードして追加されたメソッドを呼び出す処理を書きます。今回はgopper.plに対して処理を追加して行きます。

まずはpluginのロードです。

use lib file( $FindBin::RealBin, 'lib' )->stringify;
use Gopper;
Gopper->load_plugins(qw/ Method::ConfigDump /);

load_pluginsメソッドによってpluginをロードします。Method::ConfigDumpとしか書かれていませんが、Gopper::Pluginは自動補完されます。

次は追加したメソッドの呼び出しです。gopper.plのstartサブルーチンを書き換えます。

sub start {
    my $gopper = Gopper->new(config => shift);
    $gopper->call('config_dump');
    $gopper->run;
}

callメソッドで、追加されたconfig_dumpメソッドを呼び出します。Class::Component::Component::Autocall::InjectMethodなどを使用している時には、$gopper->config_dump;と簡潔に書けます。

実行してみよう!

追加したコードを実際に動かしてみましょう。gopper.plを動かすにはYAMLで記述されたPlaggerのような設定ファイルが必要です。

#config.yaml
global:
  log:
    level: debug

  engine:
    module: Simple
    config:
      host: localhost
      port: 11170

plugins:
  - module: Protocol::Gopher

今回は、ConfigDumpの動作テストをしたいだけなので必要最小限の設定にしています。

この設定を用いて実行すると以下のようになります。


$ ./gopper.pl -c config.yaml
Gopper [debug] setup engine Gopper::Engine::Simple
Gopper::Plugin::Method::ConfigDump [debug] ---
global:
  engine:
    config:
      host: localhost
      port: 11170
    module: Simple
  log:
    level: debug
plugins:
  - module: Protocol::Gopher
Gopper [debug] engine.preper

Gopper::Plugin::Method::ConfigDump [debug]の部分からconfigがdumpされました。 デーモンとして動作しているので、Ctrl-Cなどでgopper.plを強制終了して下さい。

pluginで追加されたメソッドがうまく動いた事が確認出来ましたね。この例を応用すればClass::Componentでのplugin作成は自在に出来ると思います。

Class::Component速報

今現在、Class::Componentのバージョンアップに向けた実装を行っています。予定としては、内部的な処理の変更、便利系componentの追加、pluginの初期化処理の一部変更を予定しています。それほど大きな変更にはならず、過去のバージョンとも互換性が保たれる予定です。

次回予告

今回は、Perlで利用出来るattributeの概念や活用方法、それを利用したClass::Componentでのplugin作成方法について解説を行いました。次回は、このattrbiteの話を掘り下げた話題と、Class::Componentでのattributeに独自attributeを実装する方法を紹介する予定です。

おすすめ記事

記事・ニュース一覧