#初めに
EitherT
の簡単な説明…EitherT[F[_], A, B]
は、F[Either[A, B]]
の軽量ラッパーです。Either
をOption
やFuture
などの型内に配置するような処理をする際、EitherT
を使うことによりコードをスッキリまとめやすくなります。
Scala with CatsのEitherT
について公式ドキュメントを自分なりに翻訳して説明していきます!!
※ところどころ、分かりやすいように文を付け加えてます!
#まえがき
Either
は、ほとんどの状況のエラー処理に使用できます。ただし、 Either
をOption
やFuture
などの効果のある型内に配置すると、エラーを処理するために大量のボイラープレート(仕様上省略不能で、かつほとんど変更を加えることなく多くの場所に組み込む必要があるソースコードのこと。[Wikipediaより])が必要になります。 たとえば、次のプログラムについて考えてみます。
import scala.util.Try
import cats.implicits._
def parseDouble(s: String): Either[String, Double] =
Try(s.toDouble).map(Right(_)).getOrElse(Left(s"$s is not a number"))
def divide(a: Double, b: Double): Either[String, Double] =
Either.cond(b != 0, a / b, "Cannot divide by zero")
def divisionProgram(inputA: String, inputB: String): Either[String, Double] =
for {
a <- parseDouble(inputA)
b <- parseDouble(inputB)
result <- divide(a, b)
} yield result
divisionProgram("4", "2") // Right(2.0)
// res0: Either[String, Double] = Right(2.0) // Right(2.0)
divisionProgram("a", "b") // Left("a is not a number")
// res1: Either[String, Double] = Left("a is not a number")
parseDouble
とdivide
が非同期になるように書き直され、代わりにFuture[Either[String, Double]]
を返すとします。DivisionProgram
はFuture
とEither
を一緒に構成する必要があるため、for-comprehensionは使用できなくなりました。つまり、適切な型が返されるように、エラー処理を明示的に実行する必要があります。
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
def parseDoubleAsync(s: String): Future[Either[String, Double]] =
Future.successful(parseDouble(s))
def divideAsync(a: Double, b: Double): Future[Either[String, Double]] =
Future.successful(divide(a, b))
def divisionProgramAsync(inputA: String, inputB: String): Future[Either[String, Double]] =
parseDoubleAsync(inputA) flatMap { eitherA =>
parseDoubleAsync(inputB) flatMap { eitherB =>
(eitherA, eitherB) match {
case (Right(a), Right(b)) => divideAsync(a, b)
case (Left(err), _) => Future.successful(Left(err))
case (_, Left(err)) => Future.successful(Left(err))
}
}
}
更新されたコードは明らかにより読みにくく冗長になっています。プログラムの詳細がエラー処理と混ざり合っています。そして、Either
とFuture
がさらに増えると、エラーを適切に処理するために必要なボイラープレートの量が劇的に増加します。
#EitherT
EitherT[F[_], A, B]
は、F[Either[A, B]]
の軽量ラッパーであり、Either
とF
を簡単に作成できます。EitherT
を使用するには、Either
、F
、A
、およびB
の値が最初にEitherT
に変換され、結果のEitherT
値がコンビネータを使用して合成されます。例えば、非同期除算プログラムは次のように書き直すことができます。
import cats.data.EitherT
import cats.implicits._
def divisionProgramAsync(inputA: String, inputB: String): EitherT[Future, String, Double] =
for {
a <- EitherT(parseDoubleAsync(inputA))
b <- EitherT(parseDoubleAsync(inputB))
result <- EitherT(divideAsync(a, b))
} yield result
divisionProgramAsync("4", "2").value
// res2: Future[Either[String, Double]] = Future(Success(Right(2.0)))
divisionProgramAsync("a", "b").value
// res3: Future[Either[String, Double]] = Future(Success(Left(a is not a number)))
F
がモナドの場合、EitherT
もモナドを形成し、flatMap
などのモナドコンビネータを使用してEitherT
値を作成できることに注意してください。
#A
またはB
からEitherT[F, A, B]
へ
A
またはB
が指定されたときにEitherT
のleftバージョンまたはrightバージョンを取得するには、EitherT.leftT
およびEitherT.rightT
(EitherT.pure
と同意)をそれぞれ使用します。
val number: EitherT[Option, String, Int] = EitherT.rightT(5)
val error: EitherT[Option, String, Int] = EitherT.leftT("Not a number")
#F[A]
またはF[B]
からEitherT[F, A, B]
へ
同様に、EitherT.left
とEitherT.right
を使用して、F[A]
またはF[B]
をEitherT
に変換します。また、EitherT.right
と同意なメソッドとしてEitherT.liftF
を使用することもできます。
val numberO: Option[Int] = Some(5)
val errorO: Option[String] = Some("Not a number")
val number: EitherT[Option, String, Int] = EitherT.right(numberO)
val error: EitherT[Option, String, Int] = EitherT.left(errorO)
#Either[A, B]
またはF[Either[A, B]]
からEitherT[F, A, B]
へ
EitherT.fromEither
を使用して、Either[A, B]
の値をEitherT[F, A, B]
にリフトします。F[Either[A, B]]
は、EitherT
コンストラクタを使用してEitherT
に変換できます。
val numberE: Either[String, Int] = Right(100)
val errorE: Either[String, Int] = Left("Not a number")
val numberFE: List[Either[String, Int]] = List(Right(250))
val numberET: EitherT[List, String, Int] = EitherT.fromEither(numberE)
val errorET: EitherT[List, String, Int] = EitherT.fromEither(errorE)
val numberFET: EitherT[List, String, Int] = EitherT(numberFE)
#Option[B]
またはF[Option[B]]
からEitherT[F, A, B]
へ
Option[B]
またはF[Option[B]]
は、デフォルト値とともに、EitherT.fromOption
およびEitherT.fromOptionF
にそれぞれ渡して、EitherT
を生成できます。F[Option[B]]
およびデフォルトのF[A]
には、EitherT.fromOptionM
があります。
val myOption: Option[Int] = None
// myOption: Option[Int] = None
val myOptionList: List[Option[Int]] = List(None, Some(2), Some(3), None, Some(5))
// myOptionList: List[Option[Int]] = List(
// None,
// Some(2),
// Some(3),
// None,
// Some(5)
// )
val myOptionET = EitherT.fromOption[Future](myOption, "option not defined")
// myOptionET: EitherT[Future, String, Int] = EitherT(
// Future(Success(Left(option not defined)))
// )
val myOptionListET = EitherT.fromOptionF(myOptionList, "option not defined")
// myOptionListET: EitherT[List, String, Int] = EitherT(
// List(
// Left("option not defined"),
// Right(2),
// Right(3),
// Left("option not defined"),
// Right(5)
// )
// )
val myOptionListETM = EitherT.fromOptionM(myOptionList, List("option not defined"))
// myOptionListETM: EitherT[List, String, Int] = EitherT(
// List(
// Left("option not defined"),
// Right(2),
// Right(3),
// Left("option not defined"),
// Right(5)
// )
// )
#ApplicativeError[F, E]
からEitherT[F, E, A]
へ
ApplicativeError[F, E]
またはMonadError[F, E]
は、attemptT
メソッドを使用してEitherT[F, E, A]
に変換できます。
val myTry: Try[Int] = Try(2)
val myFuture: Future[String] = Future.failed(new Exception())
val myTryET: EitherT[Try, Throwable, Int] = myTry.attemptT
val myFutureET: EitherT[Future, Throwable, String] = myFuture.attemptT
#EitherT[F, A, B]
からF[Either[A, B]]
を抽出する
基になるF[Either[A, B]]
を取得するには、EitherT
で定義されたvalue
メソッドを使用します。
val errorT: EitherT[Future, String, Int] = EitherT.leftT("foo")
// errorT: EitherT[Future, String, Int] = EitherT(Future(Success(Left(foo))))
val error: Future[Either[String, Int]] = errorT.value
// error: Future[Either[String, Int]] = Future(Success(Left(foo)))
以上で公式ドキュメントよりScala with CatsのEitherTについて説明終わりです!
ありがとうございました!!🙇♂️