概要
このページでは
-
cats.Monad
を使ってモナドを抽象化する方法 -
cats.MonadError
を使ってモナドのエラーハンドリングを抽象化する方法
を紹介します。
モナドを抽象化してできること
Scalaでコードを書いてる時、
あぁ、このメソッドどんなモナドでくるまれた値に対しても透過的に処理できるようにしておきたいなー
ってパターンありますよね?
Catsを使えば、例えば
Option
やList
といった要素数に関するモナドTry
とEither
といったエラーを表現する時に使うモナドFuture
のような非同期を表現するモナドとそれ以外のモナド
を抽象化して実装できますし、
- モナドにくるまれてる時もくるまれてないときも1本のクラスやメソッドを使いまわす
ことも可能です。(もちろんScalazでも同様のことができます)
#モナドの抽象化
catsには全てのモナドを抽象化するための型、cats.Monad
が用意されています。
cats.Monad
ここからは単純なコード例で紹介していきます。
import cats._, data._, implicits._
import scala.language.higherKinds
trait Foo[F[_]] {
def bar(fa: F[Int]): F[String]
}
class FooImpl[F[_]](implicit monad: Monad[F]) extends Foo[F] {
def bar(fa: F[Int]): F[String] = monad.map(fa)("bar" * _)
}
まず、抽象化したいモナドの型を型引数F[_]
としてしまいます。
そして、暗黙の引数にMonad[F]
を足すことでコンパイル時にCatsのMonadインスタンス
が引き当てられるようにします。
(ここではモナドの型引数とモナドインスタンスの暗黙の引数をクラス側に書いてますがメソッド側に書いてしまっても構いません。臨機応変に。)
上記のクラスの使い方と実行結果(コメント)は下記のとおりです。
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import scala.util.Try
type ThrowableOr[A] = Either[Throwable, A]
println(new FooImpl[cats.Id].bar(1))
// "bar"
println(new FooImpl[Option].bar(1.some))
// Some("bar")
println(new FooImpl[List].bar(1 :: 2 :: 3 :: Nil))
// List("bar", "barbar", "barbarbar")
println(new FooImpl[Try].bar(1.pure[Try]))
// Success("bar")
new FooImpl[Future].bar(1.pure[Future]) onComplete println
// Success("bar")
println(new FooImpl[ThrowableOr].bar(1.asRight)
// Right("bar")
いいですねCats!
クラスの型引数に型を指定するだけで任意のモナドを透過的に1本の実装で対応できています。
しかも、モナドにくるまれてない引数に対しても、cats.Id
(IDモナド)でエイリアスを張ることができています。
ここまでは、思ったとおりの実行結果になっていますね
#エラーハンドリングとMonadError
##モナドのエラーハンドリングの必要性について
しかし、実際のプログラミングでは不正な入力データや外部通信に失敗したときに当たり前のように例外が投げられてしまうものです。
class FooMightThrowImpl[F[_]](implicit monad: Monad[F]) extends Foo[F] {
def bar(fa: F[Int]): F[String] = monad.map(fa)(mightErrorProcess)
private def mightErrorProcess(a: Int): String = throw new Exception
}
上記のようにbarが例外を投げてしまう場合、それぞれのモナドでは下記の振る舞いとなります。
println(new FooMightThrowImpl[cats.Id].bar(1))
// 例外投げる
println(new FooMightThrowImpl[Option].bar(1.some))
// 例外投げる
println(new FooMightThrowImpl[List].bar(1 :: 2 :: 3 :: Nil))
// 例外投げる
println(new FooMightThrowImpl[Try].bar(1.pure[Try]))
// Failure(java.lang.Exception)
new FooMightThrowImpl[Future].bar(1.pure[Future]) onComplete println
// Failure(java.lang.Exception)
println(new FooMightThrowImpl[ThrowableOr].bar(1.asRight))
// 例外投げる (あれれ・・??)
この場合、cats.Id
Option
List
で例外を漏らしてしまうのは仕方ないとします。
気になるのは、Try
とFuture
のときは例外を漏らさずFailure
にくるまれて戻されていますが、Either
のときは例外を漏らしていることです。
Either
の場合Left側の型をThrowableとしても特にCats側で気を利かせて例外をハンドルしてくれるわけではありません。これは一見直感に反しますが、Either
は汎用的なOrを表現するモナドだからThrowableを特別視しないのは当然の振る舞いになります。
例外が投げられてくるような処理のエラーハンドリングは自前で考慮して実装しなければいけません。
これに対応するために、Catsではcats.MonadError
が用意されています。
cats.MonadError
cats.MonadError
は、エラーが発生する可能性があるモナドを抽象化したものでcats.Monad
を継承しています。
コード中では、Monad[F]
の代わりにMonadError[F, E]
を使います。
実装例は下記になります。
class FooMonadErrorImpl[F[_]](implicit monadError: MonadError[F, Throwable]) extends Foo[F] {
def bar(fa: F[Int]): F[String] = {
for {
a <- fa
res <- mightErrorProcess(a)
} yield res
}
private def mightErrorProcess(a: Int): F[String] = {
Either.catchNonFatal {
throw new Exception
} match {
case Right(ok) => monadError.pure(ok)
case Left (ko) => monadError.raiseError(ko) // raiseErrorはerror側のpureに相当する
}
}
}
Try
やFuture
、Either
のCats標準のモナドインスタンスには、既にMonadErrorの実装が用意されています。
特に、Either
に対しTry
やFuture
とエラー処理を共通化する場合は、MonadError[F, Throwable]
とするとよいでしょう。
上記を使った結果はこちら。
println(new FooMonadErrorImpl[Try].bar(1.pure[Try]))
// Failure(java.lang.Exception)
new FooMonadErrorImpl[Future].bar(1.pure[Future]) onComplete println
// Failure(java.lang.Exception)
println(new FooMonadErrorImpl[ThrowableOr].bar(1.asRight))
// Left(java.lang.Exception)
想定通り、Try
とFuture
の時はFailure
に、Either
の時はLeft
に例外がくるまれて戻されるようになりましたね
上記のようにMonadErrorで抽象化する場合、素のOption
やList
などのようにMonadErrorに対応しないモナドからは直接使えなくなりますが、モナドトランスフォーマーのEitherT
と連携することで対応すればよいでしょう。
type OptionThrowableOr[A] = EitherT[Option, Throwable, A]
println(new FooMonadErrorImpl[OptionThrowableOr].bar(EitherT.pure(1)))
// EitherT(Some(Left(java.lang.Exception)))
type ListThrowableOr[A] = EitherT[List, Throwable, A]
println(new FooMonadErrorImpl[ListThrowableOr].bar(EitherT.right(1 :: 2 :: 3 :: Nil)))
// EitherT(List(Left(java.lang.Exception), Left(java.lang.Exception), Left(java.lang.Exception)))
#最後に
今回は def raiseError[A](e: E): F[A]
しか使わない非常に単純な例で紹介しましたが、他にもcats.MonadError
のメソッドには
-
def handleError[A](fa: F[A])(f: E => A): F[A]
: 失敗内容を見て成功に変換する。(TryやFutureのrecoverに相当) -
def ensure[A](fa: F[A])(e: E)(f: A => Boolean): F[A]
: 成功の結果を条件でfilterして、false時失敗に変換する。
等があります。
OSSやプロダクションコード等ではこれらを活用しもっとしっかり作り込む必要があるでしょう。
よろしければ試してみて下さい
##参考資料
MonadErrorに関して、自分が勤める会社のCats勉強会の資料が下記に公開されています。
scala with cats: Either, MonadError, Eval Monad @ road288の日記