Error Handling Using EitherT
Are you aware of Either, if yes then you must know how it's used for error handling, but don’t you think things get messy when it is placed into effect types such as Option or Future, a large amount of boilerplate is required to handle errors.
Now let’s see this code that uses Either
import scala.util.Try def parseDouble(s: String): Either[String, Double] = Try(s.toDouble).map(Right(_)).getOrElse(Left(s"$a 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) divisionProgram("a", "b") // Left("a is not a number")Now, what if parseDouble and divide are rewritten to be asynchronous and return Future[Either[String, Double]] instead. The for-comprehension can no longer be used since divisionProgram now compose of Future and Either together.
Now our code looks like this
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)) } } }This code is messy and has less readable, and moreover has error handling logic included within the program.
Now let’s see how EitherT can be used to overcome the above issues
def divisionProgramAsync(inputA: String, inputB: String): Future[Either[String, Double]] = { val result: EitherT[Future, String, Double] = for { a <- EitherT(parseDoubleAsync(inputA)) b <- EitherT(parseDoubleAsync(inputB)) result <- EitherT(divideAsync(a, b)) } yield result result.value }EitherT[F[_], A, B] is a lightweight wrapper for F[Either[A, B]] that makes it easy to compose Eithers and Fs together.
F[Either[A, B]] can be converted into EitherT using the EitherT constructor as seen in our code above and we can use value method of EitherT to get F[Either[A ,B]] from EitherT[F[_], A, B].
Note: That when F is a monad, then EitherT will also form a monad, allowing monadic combinators such as flatMap to be used in composing EitherT values.
Lets explore more of EitherT
To obtain a left version or a right version of EitherT when given an A or a B, use EitherT.leftT and EitherT.rightT respectively.
EitherT.leftT[Option, Int]("err") res0: EitherT[Option,String,Int] = EitherT(Some(Left(err))) EitherT.rightT[Option, String](3) res0: cats.data.EitherT[Option,String,Int] = EitherT(Some(Right(3)))Similary, use EitherT.left and EitherT.right to convert an F[A] or an F[B] into an EitherT.
EitherT.right[String](Option(3)) res0: cats.data.EitherT[Option,String,Int] = EitherT(Some(Right(3))) EitherT.left[Int](Option("err")) res0: cats.data.EitherT[Option,String,Int] = EitherT(Some(Left(err)))We can use EitherT.fromEither to transform an Either to EitherT.
val either: Either[String, Int] = Either.right(3) EitherT.fromEither[Option](either) res0: EitherT[Option, String, Int] = EitherT(Some(Right(3)))We also have methods to transform an Option into EitherT.
EitherT.fromOption[List](None, "Answer not known.") res0: EitherT[List, String, Int] = EitherT(List(Left(Answer not known))) EitherT.fromOption[List](Some(42), "Answer not known.") res1: EitherT[List, String, Int] = EitherT(List(Right(42)))Here the first parameter is the Option itself whereas the second parameter is the value to be used in case Option is None.
Similarly, we can transform F[Option] to EitherT as shown below.
val o: Option[Int] = None EitherT.fromOptionF(List(o), "Answer not known.") res0: EitherT[List, String, Int] = EitherT(List(Left(Answer not known.))) EitherT.fromOptionF(List(Option(42)), "Answer not known.") res1: EitherT[List, String, Int] = EitherT(List(Right(42)))I personally believe that these various methods that EitherT provides can make your life easy while writing code that involves Either with any other effect types.
Once you start using it, you’ll also agree with me if not now.
Don’t think that EitherT is limited in the above methods, there are still many things to explore.
Hope, this blog has opened your gateway to EitherT and have left you to explore more.
Originally published at blog.knoldus.com on September 5, 2018.
