Search code examples
jsonscalaeithercirce

Is there a elegant way to handle Either Monad in Scala?


I am starting up on Scala, doing a project with circe to handle JSON.

I am coming accross a lot of Either returns from functions, and I don't seem to find a elegant way to handle all of them.

For instance, for a single either, I do as in this snippet:

if (responseJson.isRight) {
//do something
} else {
//do something else
}

But what should I do when I have a lot of them in sequence, such as this example in which I just go straight for the right side and I feel I should be doing some extra validation:

ClassA(
       someValue,
       someValue,
       someJson.hcursor.get[Double]("jsonKey1").right.get,
       someJson.hcursor.get[Double]("jsonKey2").right.get,
       someJson.hcursor.get[Double]("jsonKey3").right.get
      )

How should/can I handle multiple Either objects (without ending up with a bunch of if-elses, or similar) when I want to get their contents if they are a Right, but not I am not sure they are always a Right ?


Solution

  • Lets say you have a case class,

    case class Demo(i: Int, s: String)
    

    and two eithers,

    val intEither: Either[Throwable, Int] = ???
    val stringEither: Either[Throwable, Int] = ???
    

    So... lets start with the most basic and obvious one,

    val demoEither: Either[Throwable, Demo] = 
      intEither.flatMap(i => 
        stringEither.map(s => Demo(i, s))
      )
    

    Another way is to do the same as above is to use for-comprehensions,

    val demoEither: Either[Throwable, Demo] = 
      for {
        i <- intEither 
        s <- stringEither
      } yield Demo(i, s)
    

    But, monads are sequential, which means that if the first Either is a Left then you will not even look at the second Either and just get a Left. This is mostly undesirable for validations because you don't want to loose the validation information of all components, so what you actually want is an Applicative.

    And Either is not an Applicative, you will have to use cats or scalaz or implement your own applicative for this.

    cats provides the Validated applicative for this express purpose which lets you validate and keep all error information of the validated components.

    import cats.data._
    import cats.implicits._
    
    val intValidated: ValidatedNec[Throwable, Int] = 
      intEither.toValidatedNec
    
    val stringValidated: ValidatedNec[Throwable, String] =
      stringEither.toValidatedNec
    
    val demoValidated: ValidatedNec[Throwable, Demo] = 
      (intValidated, stringValidated).mapN(Demo)
    
    val demoEither: Either[List[Throwable], Demo] = 
      demoValidated.leftMap(errorNec => errorNec.toList)
    

    Or, if you are doing this just once and don't want to depend on cats, you can just use pattern-matching which is very versatile

    val demoEither: Either[List[Throwable], Demo] = 
      (intEither, stringEither) match {
        case (Right(i), Right(s)) => Right(Demo(i, s))
        case (Left(ti), Left(ts)) => Left(List(ti, ts))
        case (Left(ti), _) => Left(List(ti))
        case (_, Left(ts)) => Left(List(ts))
      }