理系学生日記

おまえはいつまで学生気分なのか

PoCo::Client::Twitter で POE を学ぶ

使ってみたいと思っていたフレームワークに POE がありました.ぼくは一言で POE を言い表せられるほど POE に詳しくないですけど,ドキュメント的には

POE - portable multitasking and networking framework for Perl

POE - portable multitasking and networking framework for Perl - metacpan.org

だそうで.

どんなもんなのか

POE は Perl で書いた OS,みたいなことをどこかで読んだ気がしますが,まさに OS が実行していることを Perl の世界に持ち込んだような形になっています.POE::Kernelga ノンブロッキング I/O やタイマー,シグナル処理などを提供し,(基本的には)プロセスもスレッドも作らないのに,それと同じ形で(仮想的に)並行処理を勧めることができます.

あまりわかってないぼくがアレコレいうよりも,こちらを見た方が良い.

Kernel と Session

この 2 週間くらい,合間合間にドキュメントを読んだりサンプルを読んだりしていたのですが,核となるのは POE::Kernel と POE::Session のようです.POE::Session が一つの仮想的なプロセスを表していて,複数の POE::Session で表される仮想的なプロセスを POE::Kernel が管理しているというイメージを持っています.

使ってみる

その他,特に重要そうなネームスペースと(ぼくが認識しているものと)して POE::Component と POE::Wheel がありますが,今日は POE::Component (PoCo) の一つ,PoCo::Client::Twitter を使ってみることにしました.
PoCo::Client::Twitter の example ディレクトリに入っている twitter2ircd.pl を参考にしました.参考というか劣化コピーですが.

簡単なクライアント

というわけで,PoCo::Client::Twitter を使った Twitter クライアントは次のようになりました.クライアントといっても,update はできませんし,単に標準出力にタイムラインを表示するだけですが.

#!/opt/local/bin/perl
use strict;
use warnings;
sub POE::Kernel::ASSERT_DEFAULT () { 1 }
use POE qw(Component::Client::Twitter);
use YAML;
use Getopt::Long;
use Perl6::Say;

binmode STDOUT => ':utf8';
GetOptions('verbose' => \my $verbose);

my $config = YAML::LoadFile( "./config.yaml" ) or die $!;
my $twitter = POE::Component::Client::Twitter->spawn(%{ $config->{twitter} });

my $id = POE::Session->create(
    inline_states => {
        _start => sub {
            my($kernel, $heap) = @_[KERNEL, HEAP];
            $heap->{twitter}->yield('register');
            $heap->{twitter}->yield('friend_timeline');
        },
        'twitter.friend_timeline_success' => sub {
            my($kernel, $heap, $ret) = @_[KERNEL, HEAP, ARG0];

            for my $e (@$ret) {
                my $user = $e->{user}->{screen_name};
                my $text = $e->{text};
                print "$user: $text\n";
            }
            $kernel->delay('delay_friend_timeline', $heap->{config}->{twitter}->{retry});
        },
        delay_friend_timeline => sub {
            my($kernel, $heap) = @_[KERNEL, HEAP];
            $heap->{twitter}->yield('friend_timeline');
        },
        registered => sub {},
    },
    heap => {twitter => $twitter, config => $config}
)->ID;

$poe_kernel->run();
exit 0;

POE では,"イベントに対して何を行うか" をプログラミングしていくという意味で,普通の Perl プログラミングとは異なっています.印象としては Perl/Tk とかそちらの方に近い.
もう一つ,PoCo は,POE::Session を内包した便利モジュールだとぼくは思っています.PoCo::Client::Twitter もその中に POE::Session を持っています.

ソース

今回作成したクライアントも,一つの POE::Session として実装しています.POE::Session は普通いくつかのイベントハンドラを持っています.これらのハンドラは POE::Session の create メソッドで定義します.Perl のオブジェクトにイベントハンドラを持たせることもできますが,今回は POE::Session に直接持たせることにしました.なので,create メソッドに inline_states をキーにしたイベントハンドラを渡しています.

初期状態で行う処理は _start をキーにした関数で定義します.今回の最初の状態では,PoCo::Client::Twitter に 'register' イベントと 'friend_timeline' イベントを発行しています.

        _start => sub {
            my($kernel, $heap) = @_[KERNEL, HEAP];
            $heap->{twitter}->yield('register');
            $heap->{twitter}->yield('friend_timeline');
        },

heap というのは,各 POE::Session が持っている記憶領域です.POE::Session は仮想的なプロセスだと書きましたが,この heap はそのプロセスが持つ記憶領域と考えることができます.

ここでは,PoCo::Client::Twitter のインスタンス($heap->{twitter}) へ "register" イベントを発行することで,今回作成するクライアントを PoCo::Client::Twitter にリスナとして登録し,さらに "friend_timeline" イベントを発行することで,PoCo::Client::Twitter にタイムラインを取得させます.

タイムライン取得後

PoCo::Client::Twitter は friend_timeline イベントに対し,タイムラインを取得し,リスナに対して "twitter.friend_timeline_success" イベントを発行するようになっています(PoCo::Client::Twitter は,実際には内部でいくつかのイベントを発行させつつこれらの処理を行いますが,カプセル化されているので,ぼくからはこう見えます).なので,今回の Twitter クライアントには "twitter.friend_timeline_success" イベントに対するハンドラを定義しました.

        'twitter.friend_timeline_success' => sub {
            my($kernel, $heap, $ret) = @_[KERNEL, HEAP, ARG0];

            for my $e (@$ret) {
                my $user = $e->{user}->{screen_name};
                my $text = $e->{text};
                print "$user: $text\n";
            }
            $kernel->delay('delay_friend_timeline', $heap->{config}->{twitter}->{retry});
        },

username と発言を出力した後,$heap->{config}->{twitter}->{retry} 秒後に 自身 (Twitter クライアント) に対して 'delay_friend_timeline' イベントを発行するようにしています.

        delay_friend_timeline => sub {
            my($kernel, $heap) = @_[KERNEL, HEAP];
            $heap->{twitter}->yield('friend_timeline');
        },

'delay_friend_timeline' に対するイベントハンドラでは,また PoCo::Client::Twitter に対して friend_timeline イベントを発行させています.これによって,ビジーウェイティング無し,cron 無しでタイムラインの取得を続けることができます.

次の予定

今回は明示的には POE::Session を一つしか作らなかったのですが,次は POE::Session を複数個作って仮想的な並行処理を体験したいなと思っています.