3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

EitherT[Scala with Cats]の説明

Last updated at Posted at 2020-12-29

#初めに
EitherTの簡単な説明…EitherT[F[_], A, B]は、F[Either[A, B]]の軽量ラッパーです。EitherOptionFutureなどの型内に配置するような処理をする際、EitherTを使うことによりコードをスッキリまとめやすくなります。

Scala with CatsのEitherTについて公式ドキュメントを自分なりに翻訳して説明していきます!!
※ところどころ、分かりやすいように文を付け加えてます!

#まえがき
Eitherは、ほとんどの状況のエラー処理に使用できます。ただし、 EitherOptionFutureなどの効果のある型内に配置すると、エラーを処理するために大量のボイラープレート(仕様上省略不能で、かつほとんど変更を加えることなく多くの場所に組み込む必要があるソースコードのこと。[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")

parseDoubledivideが非同期になるように書き直され、代わりにFuture[Either[String, Double]]を返すとします。DivisionProgramFutureEitherを一緒に構成する必要があるため、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))
      }
    }
  }

更新されたコードは明らかにより読みにくく冗長になっています。プログラムの詳細がエラー処理と混ざり合っています。そして、EitherFutureがさらに増えると、エラーを適切に処理するために必要なボイラープレートの量が劇的に増加します。

#EitherT
EitherT[F[_], A, B]は、F[Either[A, B]]の軽量ラッパーであり、EitherFを簡単に作成できます。EitherTを使用するには、EitherFA、および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.rightTEitherT.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.leftEitherT.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について説明終わりです!

ありがとうございました!!🙇‍♂️

3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?