この記事は Scala Advent Calendar 2015 ADVENTAR の 19日目です。
@gakuzzzz @kawachi @seratch_ja scalaz.MonadErrorで抽象化しましょう(という記事をがくぞーさんが書きましょう)
— Kenji Yoshida (@xuwei_k) 2015, 10月 27
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>
度々、失敗する可能性のある処理の戻り値を何にするべきか、という相談をされる事があります。
実際のところはやはりそのコードの背景や目的によって適切に選択するべきなのですが、 半分フレームワーク的な汎用部品や、幅広い利用が想定されているライブラリのAPIなどでは、 特定の型に依存させない方が便利になる場合があります。
そういう場合に MonadError
を使いましょうというお話です。
というかぶっちゃけ モナドの本当の力を引き出す・・・モナドによる同期/非同期プログラミングの抽象化 で書かれてる事と本質的には同じです。エラーハンドルをするにあたって Monad
では不十分な場合に MonadError
まで限定すれば対応できることもあるよという話ですね。
Scalaz のバージョンは 7.2.0 を使用します。
以下のコードは play2-auth の一部です。
def authorized(authority: Authority)(implicit request: RequestHeader, context: ExecutionContext): Future[Either[Result, (User, ResultUpdater)]] = {
restoreUser collect {
case (Some(user), resultUpdater) => Right(user -> resultUpdater)
} recoverWith {
case _ => authenticationFailed(request).map(Left.apply)
} flatMap {
case Right((user, resultUpdater)) => authorize(user, authority) collect {
case true => Right(user -> resultUpdater)
} recoverWith {
case _ => authorizationFailed(request, user, Some(authority)).map(Left.apply)
}
case Left(result) => Future.successful(Left(result))
}
}
これちょっと非同期にしたいがために Future
を使っていてエラーハンドルと話がずれてしまうんですが、他に手ごろなサンプルが思いつかなかったので。
ともあれこのコードを MonadError
で抽象化してみましょう。
とりあえず最初はシグネチャに MonadError
を導入してみます。
def authorized[F[_]](authority: Authority)(implicit request: RequestHeader, ME: MonadError[F, Throwable]): F[Either[Result, (User, ResultUpdater)]] = {
中略
}
まずメソッド自体を F[_]
で抽象化し、戻り値を F[Either[Result, (User, ResultUpdater)]]
としました。そしてその F[_]
に MonadError[F, Throwable]
という制約を加えます。ここは元々 Future
を使っていたためにエラーの型が Throwable
に固定されているからですね。元が仮に Either[Hoge, ...]
のような型ならば MonadError[F, Hoge]
にすれば良いでしょう。
また ExecutionContext
は Future
の都合なので削除しました。F[_]
に Future
を適用した場合、MonadError[Future, Throwable]
が ExecutionContext
を保持する形となります。
で、内部のコードを変えていくのですが、authorized
が Future
だったのって内部で利用しているコードが Future
だったからですね。
内部で利用しているコードのシグネチャは以下の通りです。
def restoreUser(implicit request: RequestHeader, context: ExecutionContext): Future[(Option[User], ResultUpdater)]
def authenticationFailed(request: RequestHeader)(implicit context: ExecutionContext): Future[Result]
def authorize(user: User, authority: Authority)(implicit context: ExecutionContext): Future[Boolean]
def authorizationFailed(request: RequestHeader, user: User, authority: Option[Authority])(implicit context: ExecutionContext): Future[Result]
これらも MonadError
に抽象化しましょう。
def restoreUser[F[_]](implicit request: RequestHeader, ME: MonadError[F, Throwable]): F[(Option[User], ResultUpdater)]
def authenticationFailed[F[_]](request: RequestHeader)(implicit ME: MonadError[F, Throwable]): F[Result]
def authorize[F[_]](user: User, authority: Authority)(implicit ME: MonadError[F, Throwable]): F[Boolean]
def authorizationFailed[F[_]](request: RequestHeader, user: User, authority: Option[Authority])(implicit ME: MonadError[F, Throwable]): F[Result]
これで準備が整った、と思ったら collect
メソッドに行き当たりました。 これ Future
には存在していましたが、F[_]
に抽象化した今、使えるのは制約であるところの MonadError
のメソッドだけなので存在しません。
しょうがないので定義してしまいましょう。元々の Future#collect
の挙動としては、パターンにマッチしたらそのまま変換を行い、マッチしなかったら NoSuchElementException
にして Failure
にしている感じです。
implicit class MonadErrorOps[F[_], A](val self: F[A])(implicit val ME: MonadError[F, Throwable]) {
def collect[B](pf: PartialFunction[A, B]): F[B] = self.flatMap { a =>
pf.andThen(b => ME.point(b))
.applyOrElse[A, F[B]](a, _ => ME.raiseError(new NoSuchElementException(s"MonadError.collect partial function is not defined at: $a")))
}
}
PartialFunction
が例外投げたときにcatchするかどうかは悩ましいですね。Future
の場合は flatMap
で吸収しますが。もしここで Either
などの場合でも pf
が例外投げたらエラーにしたい、という場合は以下のようにすると良いでしょう。
implicit class MonadErrorOps[F[_], A](val self: F[A])(implicit val ME: MonadError[F, Throwable]) {
def collect[B](pf: PartialFunction[A, B]): F[B] = self.flatMap { a =>
try {
pf.andThen(b => ME.point(b))
.applyOrElse[A, F[B]](a, _ => ME.raiseError(new NoSuchElementException(s"MonadError.collect partial function is not defined at: $a")))
} catch {
case NonFatal(e) => ME.raiseError(e)
}
}
}
こんな感じで collect
を定義すると、元の authorized
は以下の様にかけます。
def authorized[F[_]](authority: Authority)(implicit request: RequestHeader, ME: MonadError[F, Throwable]): F[Either[Result, (User, ResultUpdater)]] = {
restoreUser collect[Either[Result, (User, ResultUpdater)]] {
case (Some(user), resultUpdater) => Right(user -> resultUpdater)
} handleError {
_ => authenticationFailed[F](request).map(Left.apply)
} flatMap {
case Right((user, resultUpdater)) => authorize[F](user, authority) collect[Either[Result, (User, ResultUpdater)]] {
case true => Right(user -> resultUpdater)
} handleError {
_ => authorizationFailed[F](request, user, Some(authority)).map(Left.apply)
}
case Left(result) => ME.point(Left(result))
}
}
recoverWith
が handleError
に変わったのと、Future.successful
が ME.point
に変わりました。
あとは型推論の関係で collect
に型アノテーションが付いただけですね。
こうするとクライアント側は以下の様に自分の好きな型で authorize
の結果を受け取ることが可能です。
// kind-projector 使ってます
import scalaz.std.scalaFuture._
import scalaz.std.either._
val a1 = authorized[Future](authority)
val a2 = authorized[Either[Throwable, ?]](authority)
実際のアプリでは Future
を使って非同期にしつつ、テストコードでは Either
を使って同期的に処理する、みたいな事も可能ですね。
もちろん Future
と Either
だけでなく、MonadError
のインスタンスであれば何でもいいわけです。
簡単ではありますが MonadError
を使って処理を抽象化する方法をご紹介しました。
抽象化の力の一旦を感じて貰えれば幸いです。