ActionCont.recoverWithを作る

よりよい実装を作りました。http://qiita.com/yyu/items/bd6e205e801fb653a9cc

はじめに

ActionContとは継続モナドCont[R, A]の型APlayのコントローラーの結果を表す型Resultを組み合わせたCont[Future[Result], A]のことである。このActionContは継続モナドの力を用いて柔軟にコントローラーを合成するために用いられる。この記事ではまず、このActionContについて軽く紹介した後に、既存のActionContではカバーできない点について言及し、それを解決するために今回作成したActionCont.recoverWithについて説明する。

ActionContとエラー処理

ActionContとは“継続モナドを使ってWebアプリケーションのコントローラーを自由自在に組み立てる”で導入された継続モナドの一種である。このActionContにはエラーを処理するためのrecoverという関数が用意されており、それは次のようになっている。

object ActionCont { 
  def recover[A](actionCont: ActionCont[A])(pf: PartialFunction[Throwable, Future[Result]])(implicit executor: ExecutionContext): ActionCont[A] =
    ActionCont(f => actionCont.run(f).recoverWith(pf))
  }
}

これは、次のように使うことができる。

def getPostParameter(request: Request[AnyContent]): ActionCont[PostParameters] = ???

for {
  postParameters <- ActionCont.recover(getPostParameter(request)){
    case NonFatal(e) =>
      Future.successful(Results.BadRequest("error!"))
  }
} yield ???

これはエラー(つまりはFuture.failed(???))が発生し次第、即Future[Result]の値を打ち返して以降の処理をストップする。これはこれで良いが、ActionCont.recoverだけではカバーできない状況がある。

ActionCont.recoverでは難しいこと

ただし、エラーの中には回復可能なものがある。例えば次のような処理を考える。

  1. クエリパラメーターからCSRFトークンを取得する
    • 成功したら後続にCSRFトークンを渡す
  2. (1)に失敗したら、リクエストボディからJSON形式でCSRFトークンを取得する
  3. (2)に失敗したら、Results.BadRequestとなる

このような処理を書きたい場合、ActionCont.recoverを用いたとしても、直ちにResultになってしまうので実現できない。そこで、次のようなインターフェースを持つActionCont.recoverWithを作成する。

def recoverWith[A](actionCont: ActionCont[A])(pf: PartialFunction[Throwable, ActionCont[A]])(implicit ec: ExecutionContext): ActionCont[A] =

ActionCont.recoverとは部分関数として受けとる値の型が変っている。ActionCont.recoverPartialFunction[Throwable, Future[Result]]であるのに対して、ActionCont.recoverWithではPartialFunction[Throwable, ActionCont[A]]となっている。これがあれば、先ほどの処理は次のように書くことができる。

def getCsrfTokenFromQueryParameter(request: Request[AnyContent]): ActionCont[CsrfToken] = ???

def getCsrfTokenFromRequestBody(request: Request[AnyContent]): ActionCont[CsrfToken] = ???

for {
  csrfToken <- ActionCont.recoverWith(getCsrfTokenFromQueryParameter(request)) {
    case NonFatal(e) =>
      getCsrfTokenFromRequestBody(request)
  }
} yield ???

このActionCont.recoverWithをどのように実装すればよいだろうか。

仮の継続を渡してActionContを実行するfakeRun

まず、継続モナドについておさらいしておくと、継続モナドは「後続の処理を受け取って、それを使って処理を行う」という能力を持つ。そのため、ActionCont.recoverWithの実装としてシンプルに次のような実装を思いつく。

  1. 失敗するかもしれないActionContに継続を渡して実行する
    • もし成功したら、このActionContを使う
  2. 失敗したら、代わりのActionContに継続を渡す

すると、この実装では継続を合計で2回実行していることになる。確かにCSRFトークンを取得する処理ならば2回実行したところで問題はなさそうだが、もし後続の処理(継続)に「データベースに書き込む」といった副作用を伴う処理があったとしたら大変まずいことになってしまう。なので、ここではやや不完全になることが予想されるが、次のような実装を行うことにする。

  1. 失敗するかもしれないActionCont仮の継続を渡して実行する
    • もし成功したら、このActionContに本物の継続を渡す
  2. 失敗したら、代わりのActionContに継続を渡す

この仮の継続を渡す関数fakeRunは次のように定義する。

def fakeRun[A](actionCont: ActionCont[A])(implicit ec: ExecutionContext): Future[Result] =
  actionCont.run(value => Future.successful(Results.Ok))

これを使えば、ActionCont.recoverWithを作ることができる。

ActionCont.recoverWithを作る

次のような定義になる。

def recoverWith[A](actionCont: ActionCont[A])(pf: PartialFunction[Throwable, ActionCont[A]])(implicit ec: ExecutionContext): ActionCont[A] =
  fromFuture(fakeRun(actionCont).map(_ => actionCont).recover(pf)).flatten

まず、fakeRun(actionCont)で受け取ったActionContを仮に実行している。そして、その結果が成功であったとしたらmapで結果を捨てつつ元のActionContを返している。もし結果がエラー(Future.failed(???))だとしたら、Future.recoverと受け取った部分関数pfActionContにしている。すると、Future[ActionCont[A]]という型の値が得られるので、これをfromFuture1ActionCont[ActionCont[A]]にする。あとは二重になったActionContflattenで削れば最終的にActionCont[A]となる。

ActionCont.recoverWithの課題

上で述べたように、ActionCont.recoverWithに渡されるActionContはfakeRunで実行するので、後続の処理の結果によってエラーを出すようなActionContに対して使うと思わぬ挙動をする可能性がある。しかし、僕の考える限り後続の処理の結果によってエラーを出すという状況があまり考えられなかったので、実用上は問題にならないと思われる。

まとめ

やや課題が残ったものの、これによって失敗したら別のActionContに差し換えるという操作を実装することができた。もしこれより良い方法を思いついた方がいらっしゃれば、気軽にコメントなどで教えて欲しいと思う。


  1. 継続モナドを使ってWebアプリケーションのコントローラーを自由自在に組み立てるを参照。

コメント