PerlでContinuationベースのウェブサーバ

Continuationっていうのはプログラムの実行過程を途中でフリーズして別のところから再スタートさせたりすることができるという頭の痛くなるような機能だ。はやり御本家はscheme(call/cc)だけど他のハイレベル言語でもできるものがあるようだ。

ウェブフレームワークとしてはsmalltalkのseasideやcommon lispuwcが有名。RubyPythonもGeneratorを使って実装したものがあるらしい。あと、アプリケーションとしてはPaul Graham氏のnews.ycombinator.comが有名。これは彼の「100年言語」(実はPLTの上にマクロで作ったLispの方言)によって実装されている。

perlにはcall/ccやgeneratorのようなコールが無いのでContinuationベースのフレームワークは無いと思っていたが、間違っていた。Perlにできないものは滅多にない。そこで、早速PerlによるContinuationウェブアプリのHello Worldをやってみた。

Continuationのいいところはコールバック状にチマチマ千切られるロジックが通常のプログラムのように直線的に一つの関数で実装できることだ。なので、往復回数の多いフォームなどにピッタリ。項目チェックの多いユーザ登録画面をやってみた。

以下、実行可能なPerlによるContinuationウェブアプリ。インストールは下を参照。


#!/usr/local/bin/perl
use strict;
use Carp;
use Data::Dumper;

# これがパールでcontinuationを可能にする魔法。
# どうやっているか知りたくもない。
use Continuity;

# ポートを設定してサーバを走らせる。
my $server = new Continuity(port=>3000);
$server->loop;

# ディフォルトでmainという関数を走らせる。 man 3 Continutiyを参照。
sub main
{
my ($request)=@_;

# 最低限のメソッド。これだけ知っていればかなりのことができる。
# $request->print($html);
# $request->next;
# $request->param($_);
# $request->request->url->path

# メイン関数の中がアプリケーションのデータを持ている。
# この状態はリクエストを経て保たれる。
my %user_db=(tengu=>'5db05ef25fd0a77a6965b6696c5e4ad0');

# ユーザ登録フォームを見せる。
my $html=<<__END__;
<html>
<body>
<div class="message">
%s
</div>
<form>
ユーザ名<input type="text" name="user" value="%s"><br>
パスワード<input type="password" name="passwd"><br>
パスワード確認<input type="password" name="passwd2"><br>
<input type="submit" name="reg" value="登録する">
</form>
</body>
</html>
__END__
#
# ブラウザに画面を送る。
#
$request->print(sprintf($html, '', ''));

#
# ユーザ名とパスワードの検証。通るまでグルグルまわす。
#
my $user;
while(1) {
#
# ここで次のリクエストまでブロックする。
# ここが通常のウェブアプリと比べて違うところ。
# 前の$request->printとこのnextの間で
# クアライアントとこ一往復がある。
#
$request->next;

# クライアントから帰ってきた。
# リクエストパラメタはこれで見れる。
# die Dumper $request->param;

#
# 問題がある場合はメッセージを加えて、フォームを描き直す。
#
my ($passwd, $passwd2);
($user, $passwd, $passwd2)=map { $request->param($_) } qw(user passwd passwd2);

# 既存のユーザ名?
if (exists $user_db{$user}) {
$request->print(sprintf($html, '別のユーザ名を選んでください。', ''));
}
# まともなパスワードか?
elsif (length($passwd)<4) {
$request->print(sprintf($html, 'パスワードは4文字以上にしてください。', $user));
}
# パスワードはマッチしているか?
elsif ($passwd ne $passwd2) {
$request->print(sprintf($html, 'パスワードがマッチしていません。', $user));
}
# 万事オーケー
else {
use Digest::MD5 qw(md5_hex);
# ユーザをDBに挿入したりする。
$user_db{$user}=md5_hex('hoge', $passwd);
# ループ終り。
last;
}
}
#
# 登録済のユーザ様をサイトに御案内、、
#
$request->print("<html><body><center><h2>ようこそ、$user様");

# ここから落ちると、サーバのループに戻ってまた最初からやりなおし。
}


感想

ユーザと頻繁に行き来してデータベースに入れたくないような複雑なオブジェクトを作っていくには便利かなと思う。でも、通常のコールバックベースのウェブプログラミングに慣れていると、Continuationベースの流れにかえって戸惑うことの方が多いので、慣れるまでは生産性は高くないかな。

PG氏は「プログラムを短くすること」に執念をいだいているが、news.ycの簡潔さもこのcontinuation手法によるものが大きいと思う。

Continuationベースのフレームワークとは正反対に、ウェブプログラミングはfinite state machine だと認識してツールやフレームワークを使い対応していくというやり方にも興味がある。

ContinuationのアプローチもFSMの観点も使えるときがくると思うので、勉強しておきたい手法だ。

インストール

FreeBSDだと(cd /usr/ports/www/p5-Continuity/; sudo make install)で環境が揃ったと思う。
Ubuntuだとパールモジュールがパッケージ化されていないみたいなので、cpanコマンドを使ってインスートールした。
依存モジュールは:Continuity, Guard, Coro, (もしかしてEventも)