はじめに
ActionCont.recoverWithを作るでは、ActionCont.recoverWith
という次のような型を持つメソッドを作った。
def recoverWith[A](actionCont: ActionCont[A])(pf: PartialFunction[Throwable, ActionCont[A]])(implicit ec: ExecutionContext): ActionCont[A]
しかし、この関数には致命的な問題点があったので、この記事ではその問題点に関する説明と、回避するための方法について解説する。
ActionCont.recoverWith
の問題点
現在のActionCont.recoverWith
は次のように実装されている。
def fakeRun[A](actionCont: ActionCont[A])(implicit ec: ExecutionContext): Future[Result] =
actionCont.run(value => Future.successful(Results.Ok))
def recoverWith[A](actionCont: ActionCont[A])(pf: PartialFunction[Throwable, ActionCont[A]])(implicit ec: ExecutionContext): ActionCont[A] =
fromFuture(fakeRun(actionCont).map(_ => actionCont).recover(pf)).flatten
端的に述べると、ActionCont.recoverWith
は後の継続を繰り返し呼ばないように配慮したが、fakeRun
により、ActionCont.recoverWith
に入力されたActionContを2回呼び出してしまう。前回の記事で出したような副作用がないような例であると問題がなく見えるが、副作用を入れた次のような例を作ると簡単に壊れてしまう1。
import scalaz.std.scalaFuture._
def add(x: Int, y: Int): ActionCont[Int] = ActionCont(k => k(x + y))
def sideEffect(): ActionCont[Unit] =
ActionCont { k =>
println("side effect")
k(())
}
val x = ActionCont.recoverWith(for {
a <- add(1, 2)
b <- sideEffect()
} yield
Results.Ok("")
) {
case _ =>
ActionCont.successful(Results.Forbidden(""))
}
x.run_
実行すると次のように、関数sideEffect
で生成したActionContが2回実行されていることが分かる。
side effect
side effect
副作用に対して安全なActionCont.recoverWith
次のように実装する。
def recoverWith[A](actionCont: ActionCont[A])(pf: PartialFunction[Throwable, ActionCont[A]])(implicit ec: ExecutionContext): ActionCont[A] = {
class ResultContainer(val value: A) extends Result(header = ResponseHeader(200), body = Enumerator.empty)
fromFuture(actionCont.run(value => Future.successful(new ResultContainer(value))).map {
case r: ResultContainer => ActionCont[A](k => k(r.value))
case r => ActionCont.result[A](Future.successful(r))
}.recover(pf)).flatten
}
run
は実行した場合は結果の型であるPlayのResult
しか返すことができない。しかし、このようにまずResult
型のサブタイプResultContainer
を作っておき、それに継続を入れて、最後にmap
で値を取り出している。しかし、例えばActionCont.result
など継続を途中で破棄するような操作が行われている場合、我々が作ったResultContainer
が返ってこない場合がある。そこでパターンマッチを用いて、継続が途中で破棄されるような場合はそのままActionCont.result
で継続を破棄する。
以前の実装では実行して得られる値がFuture[Result]
という、ActionCont.fakeRun
で適当に入力した使い物にならない値であったが、今回はFuture[A]
という入力されたActionContが次のActionContに渡すべき値(主作用)が手に入る。入力されたActionContはこの時点で既に実行された後なので、もう一度実行はせず、さきほど得られた値を次に渡すような最小のActionContを生成して返すことにする。
このようにすることで、もし入力されたActionContの中に副作用があったとしても、一度しか実行されないので問題とならないだろう。
まとめ
このように、副作用を持つようなActionContに対しても安全にリカバーできるようになった。この記事を読んで、より良い実装を思いついた方は気軽にコメントして欲しい。