Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.

この記事はPerl 5 Advent Calendar 2016 - Qiitaの5日目です。

昨日はsago35さんによる「perl5のprintf/sprintfで*を使った書き方を学ぶ」でした。

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

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

イベント駆動プログラミングとはWikipediaによると

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

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

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

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

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

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

例えばPerl Hackers Hubにも「AnyEventでイベント駆動プログラミング」という記事があります。

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

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

この記事では

  • 日本にMojo::IOLoopを使ったイベント駆動プログラミングの記事があまりない
  • Mojo::IOLoop(とその関連モジュール)のソースコードは比較的短くかつ読みやすく、ソースコードを読んでイベント駆動がなんであるかを学べる
  • 僕自身が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をインストールします。

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

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

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

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の第2引数にイベントの内容(ユーザid, チャンネルid,テキストなど)がハッシュとして渡ってきますので頑張って実装します。
  • (4) ここでslackに接続し、イベントループをスタートさせます。

さて、これをperl hello-bot.pl TOEKNで動かすと下記のようになります。いい感じです。
Screen Shot 2016-12-05 at 23.39.42.png

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

$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");
});

なんのイベントがあるかに関しては、公式ドキュメントを参照してください。

perlのevalbotをつくってみる

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

ユーザがperl:からはじまるメッセージを投稿したとき、それをperlのスクリプトとして実行し結果を返す、evalbotを作ってみることにします。出来上がりのイメージとしては以下です。
Screen Shot 2016-12-05 at 23.52.17.png

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

同期的な実装

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

$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がCPANにありました。さすがCPAN。

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

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 バックエンドを実装してチューニングした話」です!

skaji
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away