In this blog, I will discuss about EitherT which is another concept of Scala Cats library.
Before discussing about EitherT, lets discuss Either first then EitherT which helps us in resolving the issue or boilerplate we have due to Either.
Either
Either
can be used for error handling in most situations. However, when Either
is placed into effectful types such as Option
or Future
, a large amount of boilerplate is required to handle errors
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")
If both the methods are written in asynchronous way which may return Future[Either[String, Double]] then for-comprehension can not be used. So let’s see what else we can change in above code to make it appropriate according to the requirement.
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))
}
}
}
EitherT
EitherT[F[_], A, B]
is a lightweight wrapper for F[Either[A, B]]
that makes it easy to compose Either
‘s and F
‘s together. To use EitherT
, values of Either
, F
, A
, and B
are first converted into EitherT
, and the resulting EitherT
values are then composed using combinators
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)))
There are different ways we can modify our methods so that it might return the value in different way. So lets just explore few such ways.
From A
or B
to EitherT[F, A, B]
val number: EitherT[Option, String, Int] = EitherT.rightT(5)
val error: EitherT[Option, String, Int] = EitherT.leftT("Not a number")
From F[A]
or F[B]
to EitherT[F, A, B]
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)
Extracting an F[Either[A, B]]
from an EitherT[F, A, B]
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)))
Hope this blog will help you. I am also new to this concept so have taken a reference from documentation to get a better understanding about this concept.
Happy Blogging!
References:-
