13

Suppose I have few case classes and functions to test them:

case class PersonName(...)
case class Address(...)
case class Phone(...)

def testPersonName(pn: PersonName): Either[String, PersonName] = ...
def testAddress(a: Address): Either[String, Address] = ...
def testPhone(p: Phone): Either[String, Phone] = ...

Now I define a new case class Person and a test function, which fails fast.

case class Person(name: PersonName, address: Address, phone: Phone)

def testPerson(person: Person): Either[String, Person] = for {
  pn <- testPersonName(person.name).right
  a <- testAddress(person.address).right
  p <- testPhone(person.phone).right
} yield person;

Now I would like function testPerson to accumulate the errors rather than just fail fast.

I would like testPerson to always execute all those test* functions and return Either[List[String], Person]. How can I do that ?

Xavier Guihot
  • 54,987
  • 21
  • 291
  • 190
Michael
  • 41,026
  • 70
  • 193
  • 341
  • 4
    I know this isn't what you want to hear, but error accumulation and `for`-comprehensions (or any kind of monadic sequencing) just don't mix. For example, what if you'd used `pn` in the call to `testAddress` and `testPersonName` fails? – Travis Brown Jan 25 '14 at 14:10
  • 2
    Take a look at `ValidationNel` from [`scalaz`](https://github.com/scalaz/scalaz). See [Learning scalaz/Validation](http://eed3si9n.com/learning-scalaz/Validation.html). In your case: `def tesPersonName(pn: PersonName): ValidationNel[String, PersonName] = ...` => `(testPersonName(person.name) |@| testAddress(person.address) |@| testPhone(person.phone))(Person)`. – senia Jan 25 '14 at 14:21
  • You could also take a look at [this library](https://github.com/youdevise/eithervalidation). It allows you to combine `Either` in the way you want: `Right(Person)(testPersonName(person.name), testAddress(person.address), testPhone(person.phone))`. Disclaimer: I didn't try this library. – senia Jan 25 '14 at 14:28

4 Answers4

17

You want to isolate the test* methods and stop using a comprehension!

Assuming (for whatever reason) that scalaz isn't an option for you... it can be done without having to add the dependency.

Unlike a lot of scalaz examples, this is one where the library doesn't reduce verbosity much more than "regular" scala can:

def testPerson(person: Person): Either[List[String], Person] = {
  val name  = testPersonName(person.name)
  val addr  = testAddress(person.address)
  val phone = testPhone(person.phone)

  val errors = List(name, addr, phone) collect { case Left(err) => err }

  if(errors.isEmpty) Right(person) else Left(errors)      
}
Kevin Wright
  • 49,540
  • 9
  • 105
  • 155
14

Scala's for-comprehensions (which desugar to a combination of calls to flatMap and map) are designed to allow you to sequence monadic computations in such a way that you have access to the result of earlier computations in subsequent steps. Consider the following:

def parseInt(s: String) = try Right(s.toInt) catch {
  case _: Throwable => Left("Not an integer!")
}

def checkNonzero(i: Int) = if (i == 0) Left("Zero!") else Right(i)

def inverse(s: String): Either[String, Double] = for {
  i <- parseInt(s).right
  v <- checkNonzero(i).right
} yield 1.0 / v

This won't accumulate errors, and in fact there's no reasonable way that it could. Suppose we call inverse("foo"). Then parseInt will obviously fail, which means there's no way we can have a value for i, which means there's no way we could move on to the checkNonzero(i) step in the sequence.

In your case your computations don't have this kind of dependency, but the abstraction you're using (monadic sequencing) doesn't know that. What you want is an Either-like type that isn't monadic, but that is applicative. See my answer here for some details about the difference.

For example, you could write the following with Scalaz's Validation without changing any of your individual validation methods:

import scalaz._, syntax.apply._, syntax.std.either._

def testPerson(person: Person): Either[List[String], Person] = (
  testPersonName(person.name).validation.toValidationNel |@|
  testAddress(person.address).validation.toValidationNel |@|
  testPhone(person.phone).validation.toValidationNel
)(Person).leftMap(_.list).toEither

Although of course this is more verbose than necessary and is throwing away some information, and using Validation throughout would be a little cleaner.

Community
  • 1
  • 1
Travis Brown
  • 138,631
  • 12
  • 375
  • 680
  • `:31: error: value validation is not a member of Either[String,Int]` what should I import? `myEither.disjunction.validation.toValidationNel` works fine. – senia Jan 25 '14 at 14:39
  • Oh, right—`.validation` on `Either` is [new in `7.1`](https://github.com/scalaz/scalaz/blob/scalaz-seven/core/src/main/scala/scalaz/syntax/std/EitherOps.scala). So yeah, going through a disjunction or using `Validation.fromEither` is your best bet in 7.0. – Travis Brown Jan 25 '14 at 14:41
  • As far as I remember, you should avoid catching `Throwable` in Scala. So `parseInt` should catch `Exception`: `case _: Exception => ...`. – r0estir0bbe Jun 06 '15 at 08:19
  • @r0estir0bbe Really it should be `case NonFatal(e) =>`, but I don't think it's terribly relevant to the topic. – Travis Brown Jun 06 '15 at 13:51
4

As @TravisBrown is telling you, for comprehensions don't really mix with error accumulations. In fact, you generally use them when you don't want fine grained error control.

A for comprehension will "short-circuit" itself on the first error found, and this is almost always what you want.

The bad thing you are doing is using String to do flow control of exceptions. You should at all times use Either[Exception, Whatever] and fine tune logging with scala.util.control.NoStackTrace and scala.util.NonFatal.

There are much better alternatives, specifically:

scalaz.EitherT and scalaz.ValidationNel.

Update:(this is incomplete, I don't know exactly what you want). You have better options than matching, such as getOrElse and recover.

def testPerson(person: Person): Person = {
  val attempt = Try {
    val pn = testPersonName(person.name)
    val a = testAddress(person.address)
    testPhone(person.phone)
  }
  attempt match {
    case Success(person) => //..
    case Failure(exception) => //..
  }
}
flavian
  • 28,161
  • 11
  • 65
  • 105
  • 1
    also `Either[Exception, Whatever]` is basically equivalent of `Try[Watever]` – dmitry Jan 25 '14 at 14:23
  • 2
    @dmitry - For a given definition of "basically" – Kevin Wright Jan 25 '14 at 14:24
  • @dmitry Not necessarily, you cannot always use `Try` for flow control. There are very specific usage scenarios for the two constructs. – flavian Jan 25 '14 at 14:25
  • @dmitry and @flavian So, can I use `Try` instead of `Either` in this example ? – Michael Jan 25 '14 at 14:26
  • @flavian I cannot see many cases where you would write `Either[Exception, T]` and do something that you couldn't have done with `Try[T]` instead. What can it be? – dmitry Jan 25 '14 at 14:27
  • @Michael In your case... no. As you're using Strings rather than Exceptions – Kevin Wright Jan 25 '14 at 14:27
  • @dmitry Chaining RPC calls with futures and propagating the error is a simple example. – flavian Jan 25 '14 at 14:27
  • @KevinWright I see. Suppose I change `Either[String, ...]` to `Either[Exception, ...]`. Can I replace it with `Try` now ? – Michael Jan 25 '14 at 14:29
  • @Michael, yes you can. Wrap the whole block in a `Try` and output whatever exception. – flavian Jan 25 '14 at 14:29
  • 3
    @Michael You Could make your code a little cleaner, but `Try` still doesn't have anything built-in to help specifically with the aggregation of errors. It may even by harder as you'd then find yourself with a single `Exception` representing multiple errors. – Kevin Wright Jan 25 '14 at 14:33
  • @KevinWright I understand that. Using `Try` instead of `Either` and aggregating errors instead of failing fast are two different orthogonal problems. – Michael Jan 25 '14 at 14:38
  • @flavian - Using `Try` here doesn't answer the original requirement for aggregation. – Kevin Wright Jan 25 '14 at 14:42
  • @KevinWright I know, as I said my answer is incomplete. If Michael can tell me what the usage context is(HTTP request to validate etc), I can be helpful. – flavian Jan 25 '14 at 14:51
  • I'm (only *slightly*) tempted to downvote this for not actually answering the question or the underlying requirement; but the votes suggest that you're doing *something* right... – Kevin Wright Jan 25 '14 at 15:06
  • @KevinWright Well, I upvoted your answer as it better fits the question. I'm hoping you don't give in to temptation.:) – flavian Jan 25 '14 at 15:07
  • @flavian Can't just write a smiley, it's apparently too short for a comment, so... :) – Kevin Wright Jan 25 '14 at 15:14
0

Starting in Scala 2.13, we can partitionMap a List of Eithers in order to partition elements based on their Either's side.

// def testName(pn: Name): Either[String, Name] = ???
// def testAddress(a: Address): Either[String, Address] = ???
// def testPhone(p: Phone): Either[String, Phone] = ???
List(testName(Name("name")), testAddress(Address("address")), testPhone(Phone("phone")))
  .partitionMap(identity) match {
    case (Nil, List(name: Name, address: Address, phone: Phone)) =>
      Right(Person(name, address, phone))
    case (left, _) =>
      Left(left)
  }
// Either[List[String], Person] = Left(List("wrong name", "wrong phone"))
// or
// Either[List[String], Person] = Right(Person(Name("name"), Address("address"), Phone("phone")))

If the left side is empty, then no elements were Left and thus we can build a Person out of the Right elements.

Otherwise, we return a Left List of the Left values.


Details of the intermediate step (partitionMap):

List(Left("bad name"), Right(Address("addr")), Left("bad phone"))
  .partitionMap(identity)
// (List[String], List[Any]) = (List("bad name", "bad phone"), List[Any](Address("addr")))
Xavier Guihot
  • 54,987
  • 21
  • 291
  • 190