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

第12回POE:「Perl萌え~」略ではなく

あだ名の多さは人気の証明?

POEという名前にはあきれるほど多くの寓意がこじつけられています。もともとはPerl Object Environment「Perlのオブジェクト環境」の頭文字を並べたものですが、POEの公式サイトを見てみると、Edgar Allan POE「エドガー・アラン・ポー」に始まり(そう、POEは「ポエ」ではなく「ポー」⁠ないし「ポゥ⁠⁠)と読みます⁠⁠、Parallel Object Executor「オブジェクトの並列処理機⁠⁠、Pathetically Over-Engineered「涙がちょちょ切れるほど作り込みすぎた⁠⁠、Perl Obfuscation Engine「Perl難読化エンジン⁠⁠、Perl Objects for Events「イベント用Perlオブジェクト⁠⁠、Persistent Object Environment「永続オブジェクト環境⁠⁠、Poe Organizes Everything「POEはなんでも整理する⁠⁠、Portal Of Evil「悪の玄関⁠⁠、Product Of Experts「専門家の製品」等々、よくもまあこれだけイメージをふくらませられるものだと感心する(しかも、POEの一面をよく表している)名前がたくさん紹介されています。

20世紀最後の大規模プロジェクト

POEが生まれたのは1998年8月のこと。翌1999年の8月には第3回のPerlカンファレンス(いまのOSCON)「ベスト・ニュー・モジュール」賞を、2001年春には作者のロッコ・カプト(Rocco Caputo)氏が(その前年に)Perl界に活発な貢献をした人に贈られるアクティヴ賞を受賞という具合に、POEはその誕生直後から常に注目を浴びてきました。なにしろ2000年にPerl 6の構想がぶちあげられたのは、POE以降コミュニティがわっと飛びつけるようなプロジェクトがなく、Perlの開発が停滞しているように見られたから、という見方もあるほどで[1]⁠、POEが20世紀最後の大規模プロジェクトであったのは間違いありませんし、その勢いがいまなお続いているのは前回紹介した名前空間ごとの更新頻度からもわかる通りです。

20世紀生まれのPOEが「モダン」かどうかはさておき、POEが10年来のデファクトスタンダードであることは間違いありませんし、その地位は当面揺らぐことはないでしょう。

その一方で、最近はPOEの後釜をねらうモジュールも出てきていますし、国内でもその注目度があがってきています。その善し悪しを判断するためにも、今回はPOEの基本的な使い方を簡単におさらいしておきましょう。

簡単なスクリプトをPOEで書き直してみよう

伝統的なPerlスクリプトは、どんなにオブジェクト指向的な書き方をしようと、コマンドラインから実行するときには、オブジェクトの生成、そのオブジェクトを使った処理の実行、オブジェクトの破棄といった一連の流れを順にたどっていくだけ。オブジェクトは基本的に使い捨てですし、時間のかかる処理に行き当たったらおとなしく待っているしかありませんでした。

use strict;
use warnings;

package MyObj;

sub new {
    my ($class, @files) = @_;
    bless { files => \@files }, $class;
}

sub do_tasks {
    my $self = shift;
    while ( my $file = shift @{$self->{files}} ) {
        print "processing $file\n";

        # 実際にはファイルの解析や圧縮、ダウンロードなどの
        # もっと具体的な処理が入るところです
        my $counter = int(rand(1_000_000));
        1 while $counter--;
    }
}

package main;

my $obj = MyObj->new(1..100);
   $obj->do_tasks;

POEの場合も、ごくシンプルな使い方をする分には事情はかわりません。

package main;

use POE;

POE::Session->create(
    heap => { obj => MyObj->new(1..100) },
    inline_states => {
        _start   => sub { $_[KERNEL]->yield('do_tasks') },
        do_tasks => sub { $_[HEAP]{obj}->do_tasks },
    },
);

POE::Kernel->run;

いささか見慣れない表現もありますが、ここでは速度や拡張性のため、POEが提供しているKERNELやHEAPという定数を使って@_をハッシュ的に使っています。ヒープ(heap)はセッションごとに用意されるコンテキスト/変数を入れておくところ(通常はハッシュリファレンスです⁠⁠。POE::Session->create(...) でセッションをカーネルに登録し、POE::Kernel->runで、その登録されたセッションを(すべて)実行する、というのが全体の流れになります。

セッションが始まるとまず_startに登録されている処理が実行されます(ここでは特に何もせず、そのままdo_tasksという処理に順番を譲るよう伝えています⁠⁠。do_tasksのなかではヒープに入れておいたMyObjのインスタンスにdo_tasksという処理を実行させています。それが済んだら、このセッションの仕事はすべて完了となるため、セッションが破棄され、その結果カーネルにセッションがなくなり、カーネルも終了してプログラムが終わるのですが、これだけではいかにもおもしろくありません。今度はセッションに登録する仕事の数を増やしてみましょう。

シングルタスクからマルチタスクへ

まずはセッションを登録する部分をこのように変えてみます。⁠$_[KERNEL]->delay(tick => 1);」の部分は「⁠⁠カーネルは)1秒待ったらtickに処理を譲りなさい」という意味。この場合はもう一度tickを実行しなさい、ということですね。

POE::Session->create(
    heap => { obj => MyObj->new(1..100) },
    inline_states => {
        _start   => sub {
            $_[KERNEL]->yield('do_tasks');
            $_[KERNEL]->yield('tick');
        },
        do_tasks => sub { $_[HEAP]{obj}->do_tasks },
        tick     => sub {
            print "tick\n";
            $_[KERNEL]->delay(tick => 1);
        },
    },
);

ところが、これを実際に実行してみると、do_tasksの処理が済むまでtickの動作は始まりません。もう少しマルチタスクらしい挙動をさせるためには、do_tasksの処理を分割して、ときどきPOE(のカーネル)に制御を返してやる必要があります。

use strict;
use warnings;

package MyObj;

sub new {
    my ($class, @files) = @_;
    bless { files => \@files }, $class;
}

sub do_task {
    my $self = shift;
    my $file = shift @{$self->{files}} or return;
    print "processing $file\n";
    my $counter = int(rand(1_000_000));
    1 while $counter--;
    return 1;
}

package main;

use POE;

POE::Session->create(
    heap => { obj => MyObj->new(1..100) },
    inline_states => {
        _start  => sub {
            $_[KERNEL]->yield('do_task');
            $_[KERNEL]->yield('tick');
        },
        do_task => sub {
            $_[HEAP]{obj}->do_task and $_[KERNEL]->yield('do_task');
        },
        tick    => sub {
            print "tick\n";
            $_[KERNEL]->delay(tick => 1);
        },
    },
);

POE::Kernel->run;

これでずいぶんマシになりました。do_taskの処理が終わってもtickの処理が続くのがいやなら、tickのなかに$_[HEAP]{obj}{files} の中身をチェックする処理を追加したり、ヒープのなかに実行中かどうかをあらわすフラグをひとつ用意しておけばよいでしょう。

POE::Session->create(
    heap => { obj => MyObj->new(1..100) },
    inline_states => {
        _start  => sub {
            $_[KERNEL]->yield('do_task');
            $_[KERNEL]->yield('tick');
        },
        do_task => sub {
            $_[HEAP]{obj}->do_task and $_[KERNEL]->yield('do_task');
        },
        tick    => sub {
            return unless @{$_[HEAP]{obj}{files}};
            print "tick\n";
            $_[KERNEL]->delay(tick => 1);
        },
    },
);

もちろんdo_taskの処理をさらに細分化すればより精度の高い並列処理が可能になりますが、やりすぎるとマルチタスクで得られる利点より関数コールの増加による負担のほうが大きくなるので要注意です[2]⁠。

マルチタスクからイベント駆動に

さて、ここまでくればこのスクリプトをイベント駆動にするのも簡単です。たとえば、このようにすればdo_taskの実行を1秒おきにできます[3]⁠。

POE::Session->create(
    heap => { obj => MyObj->new(1..100) },
    inline_states => {
        _start  => sub { $_[KERNEL]->yield('tick') },
        do_task => sub { $_[HEAP]{obj}->do_task },
        tick    => sub {
            return unless @{$_[HEAP]{obj}{files}};
            print "tick\n";
            $_[KERNEL]->yield('do_task') if $_[HEAP]{counter}++ % 2;
            $_[KERNEL]->delay(tick => 1);
        },
    },
);

ここでは単純にタイマーにあわせてタスクを実行しているだけですが、もちろんtickの中などでキー入力やファイルの有無、外部からのデータのやりとりなどを監視すればもっと複雑なイベントにも対応できます。

豊富な関連モジュール

ただし、実際にはこのような泥臭い処理を自分で書くことはめったにありません。POEにはすでに低レベルのものから高レベルのものまで、このようなイベント待ちを肩代わりしてくれるモジュールがふんだんに用意されているためです。

たとえば、コンソールからの入力待ちを行うようなアプリケーションであればPOEに同梱されているPOE::Wheel::ReadLineのようなモジュールを使うのが簡単でしょうし、サーバとのやりとりをともなうのであればPOE::Wheel::ReadWriteが使えます。

もっと対象がはっきりしているのであれば、たとえばPOE::Component::IRCのようなモジュールを使えばIRCクライアントが書けますし(これも裏ではPOE::Wheel::ReadWriteと、POE::Wheel::SocketFactoryというソケットまわりの処理をしてくれるモジュールが使われています⁠⁠、もっと高機能なものがよければ、たとえばBot::BasicBotBot::BasicBot::Pluggableのようなモジュールを使えば、裏に隠れているPOEの存在をあまり意識することなくIRCボットを書くこともできます。

クローラのようなものが要るなら、POE::Wheel::Runがうってつけでしょうし、もちろんこの分野でもGunghoのような高レベルのラッパが存在しています。また、日々増え続けるCPANモジュールのテストをひたすら繰り返すために、POE::Component::SmokeBoxのようなモジュールを使って継続的なテスト環境を構築している人もいます。

先ほどの例ではセッションはひとつだけでしたが、実際には(あまりお勧めできませんが)複数のセッションを登録することもできます。そのもっとも簡単な例として、Acme::POE::Kneeという競馬ゲームを行うモジュールがあります(POE::Kneeはもちろんpony「ポニー、小馬」にひっかけた表現。2003年には同じ音を持つPONIE(Perl On New Internal Engine)というプロジェクトが登場しましたし(~2006年⁠⁠、Jiftyのサンプルアプリケーションにも登場するように、ポニーはPerl界隈では非常になじみの深い生き物です⁠⁠。このAcmeモジュールはさらにPOE::Component::IRC::Plugin::POE::KneeというPOEコンポーネントを通じて、POEの公式IRCチャンネル(#poe @ irc.perl.org)上でもときどき遊ばれています。

POEの公式サイトにはほかにも用途にあわせてさまざまなレシピが紹介されています。あまりにも多岐に渡るのでいちいち概要を紹介することはできませんが、Perlでサーバやクライアントとのやりとりを含むポータブルなアプリケーションを書きたいと思ったら、まずはPOEに既存の関連モジュールやレシピがないかチェックしてみるのが早道です(POEの名前空間には400近いモジュールが登録されていますし、POEに依存しているモジュールの数も300以上あります。なかにはPOEモジュールになっている意味がよくわからないものもありますが、既存の関連モジュールが多いこともPOEの魅力のひとつです⁠⁠。

POEに対する批判

このように人気もメリットも大きいPOEですが、最初に紹介したあだ名からもわかるように、批判も少なくありません。

マルチタスクやネットワーキングに対応したプログラミングそのもののむずかしさは脇におくとしても、@_をハッシュ的に使う書き方のようにほとんどPOEでしか見られない独特の記法には抵抗を覚える人も少なくありません。公式サイトの記述にしろモジュールそのもののPODにしろ、ドキュメント類の充実振りはほかの追随を許さないほどなのですが、それでもPOEはなかなかなじめない、読み書きしづらいモジュールとしても知られています。

また、移植性の高さと引き替えに、メモリの使用量が多く、遅いという批判もありますし、安定性に疑問を投げかける声もあります。

もっとも、POEの側でも無策だったわけではありません。POEはGlibやEV、あるいはGtkやTkといったほかの(しばしばより高速な)イベントループを実装しているモジュールとも協調できるようにつくられていますし、POE::XS::Loop::EPollのようなモジュールを使えばLinuxのepoll(2)と連携をとることも可能です。

POEのわかりづらさに対しても、例によって宣言的な書き方などを使って汚い部分を隠蔽しようという試みがありますPOE::StagePOE::DeclarativePOE::DeclareMooseX::POEなど⁠⁠。

最近ではイベントループまわりのテストを改善しようという動きが進められていますし、シグナルの扱いなども着実に改善されてきています。POEのインストールについてはもともとシグナルまわりの処理が弱いWindows環境などでも問題なく入れられるようかなりの工夫がこらされていましたが、ディストリビューションとしてはいささか大きくなりすぎてきたので、外部のイベントループに依存するようなものはコアから外してもっと手軽に入れられるようにしようという動きも出てきています[4]⁠。

POEらしさからの脱却

2008年3月に晴れて正式リリース(1.000)を迎えたPOEは、いまも着実に成長しています。が、POEを使ってしまうとどうしてもアプリケーション全体がPOEを意識したつくりになってしまうことは避けられないため、最近一部の先進的なユーザの間ではPOEにかわる別のフレームワークを使う動きが活発になってきています。次回はそのあたりについて、簡単に現状をまとめてみたいと思います。

おすすめ記事

記事・ニュース一覧