初めに
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について説明終わりです!
ありがとうございました!!🙇♂️