Perl+Mojoでイベント駆動プログラミング

この記事は[Perl 5 Advent Calendar 2016 - Qiita](http://qiita.com/advent-calendar/2016/perl5)の5日目です。

昨日はsago35さんによる[「perl5のprintf／sprintfで`*`を使った書き方を学ぶ」](http://qiita.com/sago35/items/79a97f86e19c4d629e09)でした。

今日はSlackRTMを題材にPerl+Mojoでイベント駆動プログラミングをやってみたいと思います。

# イベント駆動プログラミングとは

イベント駆動プログラミングとは[Wikipedia](https://ja.wikipedia.org/wiki/%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E9%A7%86%E5%8B%95%E5%9E%8B%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0)によると

> イベント駆動型プログラミングは、起動すると共にイベントを待機し、起こったイベントに従って処理を行うプログラミングパラダイムのこと。

とのことです。プログラム上でよく扱うイベントとしては、例えば

* IOがreadできるようになった、writeできるようになった
* ファイルシステムに変更があった
* ~秒経った

があります。このようなイベントを複数同時に待ち、その起動を契機として何かの処理を行うようにプログラミングしていきます。

# Perlのイベント駆動プログラミングモジュール

Perlにはいくつかのイベント駆動プログラミングのフレームワークとなるモジュールが存在します。なかでも[AnyEvent](https://metacpan.org/pod/AnyEvent)はイベント駆動のcoreとなる部分を取り替え可能にしつつ、洗練されたAPIを提供するモジュールで高い人気があります。

例えばPerl Hackers Hubにも[「AnyEventでイベント駆動プログラミング」](http://gihyo.jp/dev/serial/01/perl-hackers-hub/000201)という記事があります。

よって今回もAnyEventを使ってもよかったのですが、あえて[Mojolicious/Mojo](https://metacpan.org/release/Mojolicious)を使ってみたいと思います。
MojoliciousはオールインワンのWeb Application Framework(WAF)であり、PerlのWAFとしてはおそらく今一番人気があり、かつactiveに開発が行われています。

一方でMojoliciousは洗練された独自のイベントループMojo::IOLoopも持っていて、これはWAFとは関係なく任意のプログラムで利用できます。

この記事では

* 日本にMojo::IOLoopを使ったイベント駆動プログラミングの記事があまりない
* Mojo::IOLoop(とその関連モジュール)のソースコードは比較的短くかつ読みやすく、ソースコードを読んでイベント駆動がなんであるかを学べる
* 僕自身が[Mojo::SlackRTM](https://metacpan.org/pod/Mojo::SlackRTM)というのを作ったのでこれの紹介もしたい

の理由からSlackRTMを題材に、Mojoを使ってイベント駆動プログラミングをやってみたいと思います。

# SlackRTM APIとは

SlackRTM API(Slack Real Time Messaging API)とはWebSocketベースのAPIで、Slackからイベントをリアルタイムで受信できて、また逆にこちらからメッセージを送ることもできます。

みなさんのSlackチームにはいくつかのbotがいると思いますが、このbotたちはだいたいこのSlackRTM APIを使って実現されています。

# SlackRTM 第一歩

それではSlackRTMで任意のメッセージに対し、helloとだけ投稿する`hello-bot.pl`を作ってみたいと思います。

まず[Mojo::SlackRTM](https://metacpan.org/pod/Mojo::SlackRTM)をインストールします。

```
$ cpanm -nq Mojo::SlackRTM
...
Successfully installed Mojo-SlackRTM-0.02
```

次にbotのアクセスを許可するためにtokenを取得します。ちょうどSlackのアドベントカレンダー[「golang で始める Slack bot 開発」](http://blog.kaneshin.co/entry/2016/12/03/162653)にtokenの取得方法がのっていたので、それを参考に取得してください。`xoxb-12345678...`のような文字列だと思います。

さあ、モジュールもインストールし、tokenも取得できたところで`hello-bot.pl`を作ります。下記のような感じです。

```perl
use Mojo::SlackRTM;

my $token = shift or die "Usage: hello-bot.pl TOKEN\n"; # (1)
my $slack = Mojo::SlackRTM->new(token => $token); # (2)

$slack->on(message => sub { # (3)
  my ($slack, $event) = @_;
  my $channel_id = $event->{channel};
  my $user_id    = $event->{user};
  my $user_name  = $slack->find_user_name($user_id);
  my $text       = $event->{text};
  $slack->send_message($channel_id => "hello $user_name!");
});

$slack->start; # (4)
```  
* (1) 引数としてtokenが渡されることを想定しています。
* (2) Mojo::SlackRTMオブジェクトを作ります。このオブジェクトにいろいろな操作をしていきます。
* (3) ここで「messageがあったら~する」を登録します。subの第２引数にイベントの内容(ユーザid, チャンネルid,テキストなど)がハッシュとして渡ってきますので頑張って実装します。
* (4) ここでslackに接続し、イベントループをスタートさせます。

さて、これを`perl hello-bot.pl TOEKN`で動かすと下記のようになります。いい感じです。
<img width="271" alt="Screen Shot 2016-12-05 at 23.39.42.png" src="https://qiita-image-store.s3.amazonaws.com/0/103710/a7201a37-a89b-58e4-3ab7-aa2be011c767.png">


なお、今は「messageがきたら~する」を登録しましたが、ここの「message」のところはいろいろ変えられます。例えば「reactionが追加されたら~する」は以下のように書けます。

```perl
$slack->on(reaction_added => sub {
  my ($slack, $event) = @_;
  my $reaction  = $event->{reaction};
  my $user_id   = $event->{user};
  my $user_name = $slack->find_user_name($user_id);
  $self->log->info("$user_name reacted with $reaction");
});
```
なんのイベントがあるかに関しては、[公式ドキュメント](https://api.slack.com/rtm)を参照してください。

# perlのevalbotをつくってみる

さて簡単なhello botが作れたところで、次はもう少し面白いものを作りたいと思います。

ユーザが`perl:`からはじまるメッセージを投稿したとき、それをperlのスクリプトとして実行し結果を返す、evalbotを作ってみることにします。出来上がりのイメージとしては以下です。
<img width="366" alt="Screen Shot 2016-12-05 at 23.52.17.png" src="https://qiita-image-store.s3.amazonaws.com/0/103710/aeb3f370-9109-d1b6-6019-812c7a3c05b1.png">


なおevalbotというものの特性上、なんでもできてしまうのでパブリックなSlack上でこのbotを動かすのはおすすめできません。

## 同期的な実装

とりあえず思いのつくのままに実装すると

```perl
$slack->on(message => sub {
  my ($slack, $event) = @_;
  my $channel_id = $event->{channel};
  my $text = $event->{text};
  if (my ($code) = $text =~ /^perl:\s*(.*)/) {
    # いくつかの文字がエスケープされているので元に戻す。
    $code =~ s/&amp;/&/g; $code =~ s/&lt;/</g; $code =~ s/&gt;/>/g;
    my $out = `perl -e '$code' 2>&1`;
    $slack->send_message($channel_id => $out);
  }
});
```

になるでしょうか。これで動くには動くのですが、問題があります。
それはバッククオートによる外部コマンドの実行はブロックするということです。
例えばユーザが`perl: sleep 10; print "hey"`というメッセージを投稿したとしましょう。すると`sleep 10; print "hey"`が実行され、バッククオートのところで10秒間ブロックします。この間、他のメッセージは受け取れません。

よってバッククオートによる外部コマンドの実行はあまりやりたくありません。

## 非同期で実装

ではどうするかといえば、forkし子プロセスで外部コマンドを実行させ、それを非同期で待つことになるでしょう。
これはなかなか骨が折れる作業ですが、実際にはすでにそれをやってくれる
モジュール[Mojo::IOLoop::ReadWriteFork](https://metacpan.org/pod/Mojo::IOLoop::ReadWriteFork)がCPANにありました。さすがCPAN。

Mojo::IOLoop::ReadWriteForkのSYNOPSISを参考にしながら書いてみます。

```perl
use Mojo::IOLoop::ReadWriteFork; # cpanmでインストールしてください
$slack->on(message => sub {
  my ($slack, $event) = @_;
  my $channel_id = $event->{channel};
  my $text = $event->{text};
  if (my ($code) = $text =~ /^perl:\s*(.*)/) {
    # いくつかの文字がエスケープされているので元に戻す。
    $code =~ s/&amp;/&/g; $code =~ s/&lt;/</g; $code =~ s/&gt;/>/g;
    my $fork = Mojo::IOLoop::ReadWriteFork->new;
    my $out = '';
    $fork->on(read => sub { # (5)
      my (undef, $buf) = @_;
      $out .= $buf;
    });
    $fork->on(close => sub { # (6)
      my (undef, $exit, $signal) = @_;
      $slack->send_message($channel_id => $out // "(no output)");
      undef $fork;
    });
    $fork->run(perl => -e => $code);
  }
});
```
* (5) forkした子プロセスからの標準出力、標準エラーを非同期で取得し、それを`$out`に連結します。
* (6) その子プロセスの終了を非同期で待ち、終了し次第`$out`をSlackに送ります。

これでブロックすることなく

* 外部コマンドを実行
* その標準出力、標準エラーを取得
* 終了を待つ

ことができました。素晴らしい。

# おわりに

この記事では、SlackRTMを題材にPerl+Mojoを使ってイベント駆動プログラミングをしてみました。

もしこれを読んで興味を持たれた方は

* Mojoの他の非同期要素(eg: timer, stream, http client)を使ってみる
* 外部コマンドの実行にタイムアウトを追加してみる
* この記事と同じことをAnyEventでやってみる

などをすると面白いと思います。

さて明日はmackee_wさんによる「PerlのELVM バックエンドを実装してチューニングした話」です！