1
votes

scalaz Validations have +++ which accumlates both errors and successes. However my success type isn't a F[T] with Semigroup[F], it's just T (unless I use the Id semigroup...). Basically I want to accumulate errors only. Is there such a method?

i.e. I have a List[A => ValidationNel[Err, A]], and I want to apply all these functions to a single A and get ValidationNel[Err, A].

2
question needs more types - Seth Tisue

2 Answers

0
votes

There are a number of options. If you have the validations as a list, then:

import scalaz.syntax.traverse._

val result: ValidationNel[Err, A] =
  validations.traverse(_(a)).map(Function.const(a))

will get you there. However if you have the validations named, then you might get more readable code with *> (or <* depending on preference):

import scalaz.syntax.applicative._

validateName(a) *> validateOther1(a) *> validateOther2(a)
0
votes

We'll need a couple of statements:

  • One can append instances of types that form a Semigroup using |+| operator.
  • Every applicative has a semigroup instance if it's parameter type has a semigroup instance.
  • ValidationNel[E,A] has an applicative instance (type parameter: A).
  • Function1: X => A has an applicative instance (type parameter: A).

So if we have several variables of type ValidationNel[E,A], and A has a semigroup, then we can append them like this:

val result: ValidationNel[E,A] = validationResult1 |+|
                                 validationResult2 |+|
                                 validationResult3

On the other side, if we have a couple of functions of type A => ValidationNel[E,A], we can append them as well (because ValidationNel[E,A] has a semigroup). If we substitute A with String, it might look like:

val isNonEmpty: String => ValidationNel[String,String] =
  str => Validation.liftNel(str)(_.isEmpty, "String is empty")
val max10Characters: String => ValidationNel[String,String] =
  str => Validation.liftNel(str)(_.length > 10, "More than 10 characters")
val startsWithUpper: String => ValidationNel[String,String] =
  str => Validation.liftNel(str)(_.headOption.exists(!_.isUpper), "Doesn't start with capital")

val validateEverything: String => ValidationNel[String,String] =
  isNonEmpty |+| max10Characters |+| startsWithUpper

Or in case of a list we can use suml function that has a reference on implicit Semigroup instance (this is equivalent):

val validateEverything: String => ValidationNel[String,String] =
  NonEmptyList(isNonEmpty, max10Characters, startsWithUpper).suml

The only problem is that default semigroup for most inward type parameter (String) is string concatenation (see scalaz.std.StringInstances.stringInstance#append) and in case when A is an arbitrary type, there might be no semigroup instance at all. We need to provide one, and in our case "take the first value" semigroup will work just fine:

implicit val takeFirst: Semigroup[A] = (a1: A, a2: Any) => a1

All above combined gives us this full test code:

import org.scalatest.{FreeSpec, Matchers}
import scalaz.{NonEmptyList, Semigroup, Validation, ValidationNel}
import scalaz.syntax.semigroup._
import scalaz.syntax.validation._
import scalaz.syntax.foldable1._
import scalaz.std.AllInstances.function1Semigroup

class ValidationSpec extends FreeSpec with Matchers {
  "accumulate validations" in {
    type A  = String
    type VF = A => ValidationNel[String, A]
    implicit val takeFirst: Semigroup[A] = (f1: A, f2: Any) => f1

    val isNonEmpty: VF =
      str => Validation.liftNel(str)(_.isEmpty, "String is empty")
    val max10Characters: VF =
      str => Validation.liftNel(str)(_.length > 10, "More than 10 characters")
    val startsWithUpper: VF =
      str => Validation.liftNel(str)(_.headOption.exists(!_.isUpper), "Doesn't start with capital")

    val allValidations = isNonEmpty |+| max10Characters |+| startsWithUpper
    allValidations("") shouldBe NonEmptyList("String is empty").failure
    allValidations("c") shouldBe NonEmptyList("Doesn't start with capital").failure
    allValidations("c2345678901") shouldBe NonEmptyList("More than 10 characters", "Doesn't start with capital").failure
    allValidations("Good value") shouldBe "Good value".successNel

    val listValidations  = NonEmptyList(isNonEmpty, max10Characters, startsWithUpper)
    val foldedValidation = listValidations.suml1
    foldedValidation("") shouldBe NonEmptyList("String is empty").failure
    foldedValidation("c") shouldBe NonEmptyList("Doesn't start with capital").failure
    foldedValidation("c2345678901") shouldBe NonEmptyList("More than 10 characters", "Doesn't start with capital").failure
    foldedValidation("Good value") shouldBe "Good value".successNel
  }
}