4

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)
    }

Mario Galic
  • 47,285
  • 6
  • 56
  • 98
yi.han
  • 369
  • 4
  • 8
  • 8
    Technically speaking, you can not. At least not using standard methods. You may write all the boiler plate to return a `Either[List[Error], Value]`. However, if you are open to other alternatives, I would suggest you using [**cats**](https://github.com/typelevel/cats). You have two options, one is to use [`Validated`](https://typelevel.org/cats/datatypes/validated.html) instead of `Either` _(which was designed for this1)_, the other one would be to use the [`Parallel`](https://meta.plasm.us/posts/2019/09/11/why-parallel/) instance of `Either` _(which in turn uses `Validated` under the hood)_. – Luis Miguel Mejía Suárez Sep 13 '19 at 13:48
  • @LuisMiguelMejíaSuárez thank yous so much. I will definitely look into these what you suggested me – yi.han Sep 13 '19 at 13:53
  • 1
    @LuisMiguelMejíaSuárez You should put that as an answer – Ethan Sep 13 '19 at 14:27
  • 2
    @LuisMiguelMejíaSuárez works exclusively in the comments section :) – Lasf Sep 13 '19 at 14:30
  • 1
    @Lasf I sometimes supplement his comment with an example and unfairly collect his upvotes for which I apologise. – Mario Galic Sep 13 '19 at 14:33
  • 1
    @MarioGalic symbiosis! – Lasf Sep 13 '19 at 14:34

1 Answers1

6

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.

Mario Galic
  • 47,285
  • 6
  • 56
  • 98
  • 2
    You know, you should not feel bad by providing good quality answers ;) - Most of the time I can not write a complete answer, either because on phone, or no time. But, as I just want to help, I try to write useful comments. So I am really happy people find they valuable and that other people can provide good answers! - BTW, right now I do not really care about the points, it was cool to reach my first 1K at that time I was also the first of the period, I remember I posted on all my social media! But, soon after that, I realized I really did not care about the points, but the feeling of helping. – Luis Miguel Mejía Suárez Sep 13 '19 at 14:43