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