kinoppyd.dev

blog

products

accounts & contact

Mobbのメソッド呼び出しをチェーンする、 chain/trigger シンタックス

posted at 2018-12-12 02:02:01 +0900 by kinoppyd

このエントリは、 Mobb/Repp Advent Calendar の十二日目です

Mobbにできないこと

これは、Mobb/Reppの未来の実装の話です。

Mobb(というよりも、Repp)の最大の特徴に、「発言を受け取り、返答を戻り値で返す」というシンプルな記述があります。これはとても強力でシンプルなルールですが、その一方でいくつか困ったことも起こります。

たとえば、Botにあるとても時間のかかる作業がさせたいとします。何も考えずに実装すると、その作業の起動トリガーを発言し、Mobbがそれを受け取り作業をし、完了したという文字列を返します。これは一見何もおかしなことは無いように見えますが、一つ大きな問題を抱えています。それは、時間のかかる作業の間、Botは無言で作業をこなしていて、その作業を起動させた人は、Botがちゃんと仕事をしているのか、それとも無言で死んだのかを区別する方法がありません。

次に、少し人間に優しい実装をしてみましょう。無言で死ぬのではなく、何かしらのエラーが発生した場合は、それを戻り値としてサービスに返せば、少なくとも何かエラーが発生したことはわかります。エラーメッセージの返し方によっては、何が原因で失敗したのかという細かい情報も得られるでしょう。これで概ね困ることは無いように思いますが、本当にそうでしょうか? この実装では、Botが作業を開始する指示を正しく受け取ってくれたのかどうかがわからないという問題が残っていませんか?

最後に、完全に人に優しい実装を考えてみましょう。作業の起動トリガーとなる発言を受けると、Botは即座に「作業を開始します」と返答します。その後、時間のかかる作業をやりながら、適宜「いまXXまで作業が終わりました」と教えてくれれば、ものすごく優しくないでしょうか? 少なくとも、コマンドを間違えてBotが起動してくれなかった場合などにはすぐ異常を検知できますし、何よりBotに愛着がわきます。

ところが、現状のMobb/Reppの構成では、このような適宜返事を返すBotと言うものを作ることが出来ません。なぜなら、最初に書いたとおり、Mobb/Reppはメソッドの戻り値がサービスの発言となり、途中経過で何かしらのアクションをすることが出来ないからです。そして、Reppはインターフェイスというその思想から、Mobb側にサービスに対してアクションをする方法は完全に秘匿されており、何もすることはできません。これは、よくあるBotフレームワークが、処理の途中で適宜サービスへのアクセスができることと大きな違いです。

しかし、Mobb/Reppは秒でクソボットを作れるフレームワークであると同時に、快適なBotを人々に提供することも軽視しているわけではありません。それでは、この問題を解決するにはどうすればいいでしょうか?

chain/trigger

回答の一つは、chain/trrigerという新しいDLSです。

これから記述するコードは、全てまだアイディア段階のもので、実際にMobb/Reppに実装されているものではありません。そのため、実際に実装されるときは形が変わる可能性もあることを了承してください。

require 'mobb'

helpers do
  def very_slow_task
    sleep(10)
  end
end

on '時間のかかる作業' do
  chain :slow_task
  "時間のかかる作業をします"
end

trigger :slow_task do
  very_slow_task
  "時間のかかる作業が終わりました"
end

通常通り、onでサービスからの発言を受け取り、文字列を返すところまでは変わりません。しかし、onのブロックの中で、chainという一つの引数を取る今までに無いメソッドを呼び出しています。

そして、triggerという新しいDSLが追加されています。このtriggerは、一つの引数を受け取り、ブロックを取ります。これは、onやcronと同じです。

おそらく直感的にわかると思いますが、onの中で呼び出されたchainの引数であるシンボルと一致するtriggerが、onの終了後に呼び出されます。フロー的には次のようになります。

  1. onブロックの処理が開始される

  2. chainで、:slow_taskという連鎖を追加する

  3. onブロックが終了し、Reppが戻り値(ここでは「時間のかかる作業をします」という文字列)をサービスに投稿します

  4. Reppが、chainで登録された情報をもとに、再度Mobb側に情報を渡します。このとき渡される情報は、最初のonブロックに入ったときと等しく扱える情報が渡されます

  5. Mobbは、:slow_task というtriggerが起動されたことを検知し、triggerのブロックを実行します。ブロックの中では、very_slow_taskというヘルパーが呼び出されます

  6. triggerブロックが終了し、Reppがの戻り値(ここでは「時間のかかる作業が終わりました」という文字列)をサービスに投稿します

これまでのMobb/Reppであれば、1から3までの手順のみが実行可能でした。しかし、次のバージョンのMobb/Reppからは、処理をchain/triggerで連鎖させることで、ある作業の途中経過にサービスにアクセスするという処理を表すことができます。

require 'mobb'

helpers do
  def very_slow_task
    sleep(10)
  end
end

on '時間のかかる作業' do
  chain :slow_task_1
  "時間のかかる作業をします"
end

trigger :slow_task_1 do
  very_slow_task

  chain :slow_task_2
  "最初の時間のかかる作業が終わりました"
end

trigger :slow_task_2 do
  very_slow_task
  "次の最初の時間のかかる作業が終わりました"
end

このように、triggerのなかからさらにchainすることも可能にする予定です。

require 'mobb'

helpers do
  def very_slow_task
    sleep(10)
  end
end

on '時間のかかる作業' do
  chain :slow_task_1, :slow_task_2
  "時間のかかる作業をします"
end

trigger :slow_task_1 do
  very_slow_task
  "最初の時間のかかる作業が終わりました"
end

trigger :slow_task_2 do
  very_slow_task
  "次の最初の時間のかかる作業が終わりました"
end

そしてこのように、chainを使って複数のタスクを並列で連鎖させることもできるようにしようと思います(これはExperimentalで、本当に可能かどうかはわかりません。というのも実現は可能ですが、Repp側で正しく制御することを強制できるかどうかはわからないためです)。

Mobb 0.5.0

この機能は、次のリリースのMobbで実装される予定です。次のバージョンは、年内に出ればいいなくらいの気持ちで考えています。

本当は、最初はこの機能のキーワードは then/kick という名前にしようと思ってたんですが、Ruby 2.6.0 から yield_self が then というaliasで突っ込まれたため(コミッターの人たちはthen then いいネーミングじゃないとRubyKaigiで言ってたのに、対案が出なかったんだな……)、このキーワードは諦めて chain/trigger になりました。