Pixel Pedals of Tomakomai

北海道苫小牧市出身の初老の日常

Coro::AnyEventでフロントエンドをコールバックを使わずに書く

Coro::AnyEventを使うと、継続渡しスタイルを使わずに非同期を書くことができます*1

ということで、試しにやってみました。

最初にまとめ

AnyEventの層を作り、その上にCoroの層を重ねて、フロントエンドからはCoroの層だけ使うとわかりやすいアプリができるんじゃないかなあと思います。

(0) AnyEventのAPIを作る

まず、AnyEventでフロントエンドのための材料を作ります。頑張ってコールバックを使って非同期処理を書いて下さい。名前空間はAnyEvent::的な何かにするといいと思います。また、たぶんこの段階ではまだCoro::AnyEventは使わない方が無難です。ピュアAnyEventの方が使い回しが効きますし。

ここでは、AnyEvent::HTTPを題材にします。バックエンドとしてすでにAnyEvent::HTTPは使えるものとします。

(1) CoroのAPIを作る

前に書きましたが、Coroでコールバックを隠すのは簡単です。この方法でCoroのAPIの層を作ります。こいつは名前空間をCoro::にするなどして、(0)のAnyEventのAPI層とは分けるといいでしょう。

package Coro::HTTP;
use strict;
use warnings;
use AnyEvent::HTTP;
use Coro ();
use Coro::AnyEvent ();

sub coro_get{
	my ( $url ) = @_;
	http_get $url, Coro::rouse_cb;
	return Coro::rouse_wait;
}

# .. 他にも色々 ..

これで継続渡しスタイルが、普通のreturnスタイルになりました。なお、バックエンドがAnyEventですので、Coro::AnyEventをuseしておいた方がいいです。

(2) CoroのAPIを使う

(1)で作ったAPIを使います。Coro::AnyEventの効果で、AnyEventのイベントループは背後のスレッドでこっそり動いてくれます。なので、メインスレッドから(1)のAPIを使えば、勝手にイベントループが回って非同期処理になります。

#!/usr/local/bin/perl
use strict;
use warnings;
use Coro;
use Coro::HTTP;
use Web::Scraper;
use Encode;
use Encode::Guess qw/euc-jp cp932 ascii utf8/;

my @coros;
my %titles;
for my $url (
	'http://d.hatena.ne.jp/hiratara/',
	'http://www.century21.co.jp/',
	'http://www.yahoo.co.jp/',
	'http://www.google.com/',
){
	push @coros, async {
		my ($body, $hdr) = Coro::HTTP::coro_get $url;

		my $scraper = scraper {
			process 'title', 'title' => 'TEXT';
		};

		my $title = $scraper->scrape( $body )->{ title };
		$titles{$url} = encode_utf8( decode "Guess", $title );
	};
};

$_->join for @coros;

use Data::Dumper;
print Dumper \%titles;

Coroの層のおかげで、フロントエンドは既存のコーディングスタイルと同じように組むことができました。違うのはasyncとjoinが入っていることだけです。IOやネットワークの待ち時間があると思われる部分を、asyncブロックで複数同時に走らせ、joinでそれらが終わるのを待ちます。

そして実は、asyncやjoinがなくても、Coroの層(この処理)は動きます。その場合は非同期処理ではなく同期的な処理にはなりますが、要はCoroの層は普通の関数として何も考えずに使うことができるということです。陰(AnyEventの層)にあるコールバックの仕組みは完全に隠蔽されています。

Coroの層が厚くなっても、フロントエンドには特に問題は起きません。Coroの層は普通に値を返す関数だと思ってコーディングすることができます。これらの関数は、呼んだ時に必要に応じて別スレッド(別のasyncブロックやイベントループ)へ処理を移してくれますので、非同期的に実行されます。

これと対照的にAnyEventをそのまま使っていると、コールバックが増えれば増えるほどクロージャのネストが深くなって処理が書きにくくなっていきます。

なお、(2)でフロントエンドの処理を書く時は(0)のバックエンドとは逆に、AnyEventは直接使わない方が無難だと思います。理由は、AnyEventのイベントループからはCoroの層の関数が使えない*2からです。その辺りをわかってて使えば大丈夫なのかもしれませんが、「バックエンドはAnyEventのイベントループのスレッド、フロントエンドはそれ以外のスレッド(メインスレッドも含む)を使う」と分けた方がわかりやすい気がします。

注意点

この方法は、AnyEventの層が主に「コールバックに1度だけ値を返却する処理」でないと使いにくいです。GUIのイベントを拾ったり、IRCクライアントのように登録したコールバックが何度も呼ばれるパターンでは、return型の関数にするのは難しいと思います*3

*1:参考: Coro と AnyEventの関係

*2:特にrouse_wait使っちゃ駄目。イベントループのスレッドに移ってループを回したいのに、自分がそのスレッドなので。イベントループのスレッドは勝手に眠らせちゃいけません。

*3:while でループさせるような作りにしたらできそうですけど、それがわかりやすいかどうか。