/ Cats/Scala

Validated & ValidatedNel

In a previous post I explained that Cartesians and Applicatives are used to perform N independent operations. One such example is validation. We typically want to validate multiple fields and accumulate errors. Cats includes a data type designed specifically for this. It's called Validated

You can find complete examples of the concepts discussed on my blog in my GitHub repo. In particular, check out the ValidatedNel example

Why do we need Validated?

In theory we could use Either to catch validation failures then use a Cartesian or Applicative to aggregate them:

import cats.Applicative
import cats.instances.either._
import cats.syntax.either._

type Error = String
type ErrorOr[A] = Either[Error, A]

val errorOrFirstName = "john".asRight[Error]
val errorOrLastName = "doe".asRight[Error]
val errorsOrFullName: ErrorOr[String] = 
  Applicative[ErrorOr].map2(errorOrFirstName, errorOrLastName)(_ + " " + _)

println(errorsOrFullName)

output:
Right(john doe)

This will compile and run, producing Right(john doe) but look carefully at the type of errorsOrFullName, ultimately it's Either[String, String]. If one of the validations fails we will of course get a Left:

...
val errorOrFirstName = "empty first name".asLeft[String]

output:
Left(empty first name)

what happens if both validations fail?

...
val errorOrFirstName = "empty first name".asLeft[String]
val errorOrFirstName = "empty last name".asLeft[String]

output:
Left(empty first name)

hmm, it's not what we want. Maybe the problem lies in the type used for the left side of the Either. It's a String, but we want a collection of errors. Let's try using a List:

type Error = String
// use a List on the left side
type ErrorOr[A] = Either[List[Error], A]

val errorOrFirstName = List("empty first name").asLeft[String]
val errorOrLastName = List("empty last name").asLeft[String]
val errorsOrFullName: ErrorOr[String] = 
  Applicative[ErrorOr].map2(errorOrFirstName, errorOrLastName)(_ + " " + _)

println(errorsOrFullName)

output:
Left(List(empty first name))

It still doesn't work! The code is failing fast on the first error, in fact it's no better than using trivial for-comprehension/flatMap. What's happening?

The type class hierarchy

To understand what's happening we need to understand that cats includes a hierarchy of type classes which looks something like this:

Cartesian/Semigroupal -> Applicative -> Monad

Ultimately we're instantiating an Applicative instance for Either[List[String], String]. The problem is that Cats actually provides a Monad instance for Either. Given the hierarchy, this is also an Applicative so it compiles fine. Behind the scenes our map2() call results in a call to product(), which in turn calls flatMap() (on a Monad).

This explains the weird behavior. Our map2() call on the Applicative results in a flatMap() call on a Monad. So what's the solution?

We need something that is like Either but only has Cartesian/Applicative type class implementations, no Monad implementation. The answer is the Validated data type. By using Validated instead of Either we can be sure that we are actually dealing with an Applicative, not a Monad pretending to be an Applicative!

Using Validated

Let's rewrite our previous example to use Validated instead of Either:

import cats.Applicative
// 1. use Validated
import cats.data.Validated
// 2. this gives us .invalid and .valid which are
// comparable to .asLeft and .asRight for either
import cats.syntax.validated._
// 3. we now also need a Semigroup instance for list, see below
import cats.instances.list._

type Error = List[String]
type ErrorOr[A] = Validated[Error, A]

val errorOrFirstName = List("empty first name").invalid[String]
val errorOrLastName = List("empty last name").invalid[String]
val errorsOrFullName: ErrorOr[String] = 
  Applicative[ErrorOr].map2(errorOrFirstName, errorOrLastName)(_ + " " + _)

println(errorsOrFullName)

output:
Invalid(List(empty first name, empty last name))

We've made a few changes:

  1. We're now using Validated instead of Either
  2. We've pulled in the syntax extensions for Validated instead of Either. This allows us to use something like "john".valid or List("empty name").invalid
  3. We've also imported cats.instances.list._. Our code now does what we want, it will accumulate errors instead of failing fast. But how do we aggregate errors from multiple lists? We need a Semigroup instance for List which will allow the underlying applicative to call combine()

The last point is significant. I've chosen to use a List to accumulate errors as it's a logical choice. I could actually have used any type so long as I bring a Semigroup instance for it into scope (or define my own). For example I could use simple Strings:

import cats.data.Validated
import cats.syntax.validated._
// pull in a Semigroup instance for String
import cats.instances.string._

// use a String on the left side
type Error = String
type ErrorOr[A] = Validated[Error, A]

val errorOrFirstName = "empty first name".invalid[String]
val errorOrLastName = "empty last name".invalid[String]
val errorsOrFullName: ErrorOr[String] = 
  Applicative[ErrorOr].map2(errorOrFirstName, errorOrLastName)(_ + " " + _)

println(errorsOrFullName)

output:
Invalid(empty first nameempty last name)

Ok, it's a bit nasty because the default Semigroup instance for String simply concatenates the values without spaces but we could write our own implementation:

// remove import cats.instances.string._ and use our own
implicit val stringSemigroup = Semigroup.instance[String](_ + ", " + _)

output:
Invalid(empty first name, empty last name)

Bonus features

I mentioned that Validated is basically just like Either except there is no Monad type class for it. Validated also includes a few little helpers which are similar to the extensions Cats includes for the standard Either type:

Validated.valid[A, B]()
Validated.invalid[A, B]()
Validated.catchOnly()
Validated.fromEither()
validated.toEither(), toOption(), toList() etc

ValidatedNel

Like Either, Validated represents success or failure. It should be binary (Valid or Invalid). However if we use a List to accumulate failures we throw away some type safety. What does an Invalid holding an empty List mean?

For this reason many developers use Cats' NonEmptyList to accumulate errors. Using this means we can guarantee the binary semantics that we desire.

Cats incudes a data type called ValidatedNel which is simply a type alias for Validated[NonEmptyList[E], R] and it also adds a few helpers to the Validated and syntax objects e.g.

import cats.Applicative
import cats.data.ValidatedNel
import cats.syntax.validated._

type Error = String
// use a NonEmptyList on the left side
type ErrorOr[A] = ValidatedNel[Error, A]

// using invalidNel is a shortcut for
// NonEmptyList.of("empty first name").invalid[String]
val errorOrFirstName = "empty first name".invalidNel[String]
val errorOrLastName = "empty last name".invalidNel[String]
val errorsOrFullName: ErrorOr[String] =
  Applicative[ErrorOr].map2(errorOrFirstName, errorOrLastName)(_ + " " + _)

println(errorsOrFullName)

output:
Invalid(NonEmptyList(empty first name, empty last name))