yi.han
yi.han

Reputation: 379

How to yield all errors (failures) with Either

I am completely new to Scala. AFAIK, Either encapsulate failure handling allowing chain operations without writing boilerplate code repeatedly. It allows also circuit break the continuation of execution. But this may not always what I want. e.g. for the following code, if both name and age are invalid, the makePerson function will not return both errors.

Can you guys please suggest a way?

case class Person(name: Name, age: Age)
sealed class Name(val value: String)
sealed class Age(val value: Int)

case class Person(name: Name, age: Age){

}
sealed class Name(val value: String)
sealed class Age(val value: Int)

object Person{
    def makeName(name: String): Either[String, Name] = {
        if (name == "" || name == null) Left("Name is empty.") else Right(new Name(name))
    }

    def makeAge(age: Int): Either[String, Age] = {
        if (age < 0) Left("Age is out of range.") else Right(new Age(age))
    }

    def makePerson(name: String, age: Int): Either[String, Person] = {
        mkName(name).map2(mkAge(age))(Person(_, _))
    }
}
    def map2[EE >: E, B, C](b: Either[EE, B])(f: (A, B) => C): Either[EE, C] = {
            val func = (aa: A) => b.map(bb => f(aa, bb))
            this.flatMap(func)
    }


    def flatMap[EE >: E, B](f: A => Either[EE, B]): Either[EE, B] = this match {
        case Left(e) => Left(e)
        case Right(a) => f(a)
    }

Upvotes: 5

Views: 827

Answers (1)

Mario Galic
Mario Galic

Reputation: 48410

Here are examples of error accumulation using Validated and Parallel as suggested by @Luis:

Validated

import cats.data.ValidatedNec
import cats.implicits._

case class Person(name: Name, age: Age)
case class Name(value: String)
case class Age(value: Int)

object validatedPerson extends App {
  private def validateName(name: String): ValidatedNec[String, Name] =
    if (name == "" || name == null) "Name is empty.".invalidNec else Name(name).validNec

  private def validateAge(age: Int): ValidatedNec[String, Age] =
    if (age < 0) "Age is out of range.".invalidNec else Age(age).validNec


  def validatePerson(name: String, age: Int): ValidatedNec[String, Person] = {
    (validateName(name), validateAge(age)).mapN(Person)
  }

  println(validatePerson(name = "Joe", age = 21))
  println(validatePerson(name = "", age = -42))
}

which outputs

Valid(Person(Name(Joe),Age(21)))
Invalid(Chain(Name is empty., Age is out of range.))

where we see in the Invalid case all the errors were accumulated.

Parallel

import cats.data.{EitherNel, NonEmptyList}
import cats.instances.parallel._
import cats.syntax.parallel._

object parValidatedPerson extends App {
  private def validateName(name: String): EitherNel[String, Name] =
    if (name == "" || name == null) Left(NonEmptyList.one("Name is empty.")) else Right(Name(name))

  private def validateAge(age: Int): EitherNel[String, Age] =
    if (age < 0) Left(NonEmptyList.one("Age is out of range.")) else Right(Age(age))


  def validatePerson(name: String, age: Int): Either[NonEmptyList[String], Person] = {
    (validateName(name), validateAge(age)).parTupled.map(Person.tupled)
  }

  println(validatePerson(name = "Joe", age = 21))
  println(validatePerson(name = "", age = -42))
}

which outputs

Right(Person(Name(Joe),Age(21)))
Left(NonEmptyList(Name is empty., Age is out of range.))

where we see in the Left case all the errors were accumulated.

Upvotes: 8

Related Questions