はじめに
最近Slack Bot作りに興味を持ち、どうせSlack Botを作るならば最も使い慣れた言語であるScalaで作りたくなった。また、継続モナドを使ってWebアプリケーションのコントローラーを自由自在に組み立てるを読んで、継続モナドを利用することでSlack Botもより柔軟に作れるのではないかという考えのもと、Scalaを利用して作ってみた。この記事では、まずこのSlack Botの動かし方を説明し、すごく簡単にこのSlack Botでどのように処理を書くかについて述べる。
発表資料
この内容について発表した資料が下記にある。
動かしてみる
次の手順で動かすことができる。
- リポジトリをGitで持ってくる
./src/main/resources/default.conf
をエディターで開く次の
token
のところにSlack Botのトークンを書き込むslack { token = "<Your Slack Bot Token>" duration = "5" }
<Your Slack Bot Token>
を消して書き換える
Javaをインストールして
./sbt run Main
を実行する
中身の説明
SlackCont
まず、SlackCont
というデータ構造について説明する。これは次のように定義されている。
case class SlackEnv(client: SlackClient, ec: ExecutionContext)
type SlackCont[A] = SlackEnv => ContT[Future, Unit, A]
これはSlackにメッセージを送信したりするSlackClient
と、Future
を操作するために利用するExecutionContext
の組であるSlackEnv
という型を引数にとって継続モナドContT
を返す関数のエイリアスである。関数にせずReaderT
でもいいかと思ったが、モナドが重なりすぎていて大変になると思い、このようにした。
このモナドを利用して、次のようなSlack Botをどう書けるのかを見てみる。
- メッセージの内容が
Hello
かどうかを検査する - もしメッセージが
Hello
ならば書き込み中ステータスにする - 2秒間待機する
World
という文字列を投稿する
HelloWorldCont
もしメッセージがHello
に一致するならば継続にWorld
を渡す処理は次のように書ける。
object HelloWorldCont {
def apply(message: Message): SlackCont[String] = SlackCont[String] { env =>
implicit val ec: ExecutionContext = env.ec
k =>
for {
_ <- if (message.text == "Hello") {
k("World")
} else {
Future.successful(())
}
} yield ()
}
}
このようにHello
でなかった場合は継続k
を利用せず、そのまま成功を返して終了する。
TypingCont
Slackでタイピング状態を送信する処理は次のように書ける。
class TypingCont @Inject()(
threadSleep: ThreadSleep
) {
def apply(channelId: String, msec: Long): SlackCont[Unit] = SlackCont { env =>
implicit val ec: ExecutionContext = env.ec
k =>
env.client.indicateTyping(channelId) // 失敗を無視する
threadSleep.sleep(msec)
k(())
}
}
まず、スリープする処理はテストのときに邪魔なのでGuiceでDIしている。そして、Slackクライアントにタイピングする命令を送り、その結果に関わらず継続を実行する。
SayCont
引数に渡されたチャンネルに、メッセージを投稿する処理は次のように書ける。
object SayCont {
def apply(sendChannelId: String, sendMessage: String): SlackCont[Long] = SlackCont[Long] { env =>
implicit val ec: ExecutionContext = env.ec
k =>
for {
n <- env.client.sendMessage(SlackSendMessage(sendChannelId, sendMessage))
r <- k(n)
} yield r
}
}
この処理ではenv.client.sendMessage
はFuture[Long]
を返すので、もし投稿に失敗してFuture.failed
となった場合にはFuture
のflatMap
が失敗し継続は実行されない。
Main
それではこれらを組み合せて実際に動くようにしてみよう。
val injector = Guice.createInjector(new DefaultModule)
val slackRunner = injector.getInstance(classOf[SlackRunner])
val typingCont = injector.getInstance(classOf[TypingCont])
slackRunner.onMessage(msg =>
for {
world <- HelloWorldCont(msg)
_ <- typingCont(msg.channel, 2000)
_ <- SayCont(msg.channel, world)
} yield ()
)
このように、比較的直感的に処理を組み合わせていくことができる。
まとめ
このように継続モナドを組み合せてSlack Botを作ってみたが、まだ機能がそんなにないので継続を利用したおもしろい処理を書けていない。もしそういう機能を実装できたら、また追記したいと思う。
コメント
コメントを投稿