# Either, Validated, and Parallel
When it comes to validating values, many people working with Scala will think of the Validated (opens new window) data type
from the Cats (opens new window) library. Indeed, as the name says, it's very well suited for such kind of problems. In this post,
I will explain why it might be not the best idea to use Validated
directly and how you can achieve the same results
with the Either
data type from the standard library.
# Validated
As a small reminder, the Validated
data type is defined as follows:
sealed abstract class Validated[+E, +A]
final case class Valid[+A](a: A) extends Validated[Nothing, A]
final case class Invalid[+E](e: E) extends Validated[E, Nothing]
The Valid
case represents a value that passed the validation, and the Invalid
case contains an error. There is also
a type alias
type ValidatedNec[+E, +A] = Validated[NonEmptyChain[E], A]
that allows to accumulate multiple errors (NonEmptyChain (opens new window) is a data structure that can never be empty and that supports efficient prepending and appending).
A useful property of Validated
is that it has an instance of the Applicative (opens new window) type class which combines
multiple validated values accumulating possible errors.
Let's start with a small example of input validation. Imagine we are building a car marketplace. When someone wants to sell a car, we check if the car make and model make sense, and also we want people to provide a meaningful description of the car.
case class Make(name: String)
case class Model(name: String)
case class Description(text: String)
case class MakeModel(make: Make, model: Model)
case class Listing(makeModel: MakeModel, description: Description)
Using Validated
, we can write the following code:
def validateMake(make: String): ValidatedNec[String, Make]
def validateModel(name: String): ValidatedNec[String, Model]
def validateDescription(text: String): ValidatedNec[String, Description]
def validateMakeModel(make: Make, model: Model): ValidatedNec[String, MakeModel]
def validateListing(makeName: String,
modelName: String,
descriptionText: String): ValidatedNec[String, Listing] = {
val make = validateMake(makeName)
val model = validateModel(modelName)
val description = validateDescription(descriptionText)
val makeModel = (make, model).tupled
.andThen { case (make, model) => validateMakeModel(make, model) }
(makeModel, description).mapN(Listing)
}
The Applicative
instance for the Validated
data type allows us to use tupled
and mapN
to combine validation
results. When we call validateMakeModel
we need something different though — we need to use already validated make
and model
. Luckily, there is an andThen
method that does exactly this.
A slightly simplified signature of andThen
is
def andThen[B](f: A => Validated[E, B]): Validated[E, B]
and it looks exactly as flatMap
would look like. Unfortunately, andThen
cannot be just renamed to flatMap
. The
latter is an operation from the Monad
type class, and since cats.Monad
extends cats.Applicative
there is a law
that defines how their operations must relate to each other. One of them says that Applicative
operations must be
equivalent to the same operations implemented using Monad
operations. In the case of Validated
this means that if
there was an instance of the Monad
type class, the Applicative
operations wouldn't be able to accumulate errors.
# Either
So, when we write a function that returns Validated
we, in fact, say that the callers of our function want to
accumulate errors (through the Applicative
instance) and if they need to chain computations they will have to do
something special. This sounds like an arbitrary assumption — we might as well return Either
that supports flatMap
(through the Monad
instance) and require special handling for the accumulating use case:
def validateMake(name: String): EitherNec[String, Make]
def validateModel(name: String): EitherNec[String, Model]
def validateDescription(text: String): EitherNec[String, Description]
def validateMakeModel(make: Make, model: Model): EitherNec[String, MakeModel]
def validate(makeName: String,
modelName: String,
descriptionText: String): EitherNec[String, Listing] = {
val make = validateMake(makeName)
val model = validateModel(modelName)
val description = validateDescription(descriptionText)
val makeModel = (make, model).parTupled
.flatMap { case (make, model) => validateMakeModel(make, model) }
(makeModel, description).parMapN(Listing)
}
As you can see, the code looks very similar to the version written with Validated
: instead of ValidatedNec
I used
EitherNec
which is a type alias for Either[NonEmptyChain[E], A]
, and instead of tupled
and mapN
I used
parTupled
and parMapN
. Let's look at these two methods in more detail.
A naive implementation of an error-accumulation makeModel
validation for Either
could look like this:
val makeModel =
(make.toValidated, model.toValidated).tupled.toEither
.flatMap { case (make, model) => validateMakeModel(make, model) }
We convert Either
to Validated
, use .tupled
as before to accumulate errors, and then convert the result back to
Either
.
If you check the implementation of the parTupled
and parMapN
functions you will see that they require a
Parallel (opens new window) type class (or, more precisely, NonEmptyParallel
which is a superclass of Parallel
). Parallel
establishes a relation between Monads
that describe dependent computations, and Applicatives
that describe
independent computations:
trait Parallel[M[_]] {
type F[_]
def parallel: M ~> F
def sequential: F ~> M
}
where M[_]
has to have an instance of Monad
and F[_]
an instance of Applicative
. For example, an instance of
Parallel
for the Either
data type uses Validated
as an error-accumulating counterpart. With this knowledge we can
rewrite the code that uses toValidated
and toEither
functions into a more generic form:
val P = Parallel[EitherNec[String, *]]
val makeModel =
P.sequential((P.parallel(make), P.parallel(model)).tupled)
.flatMap { case (make, model) => validateMakeModel(make, model) }
And that's exactly what parTupled
does under the hood, so the final implementation is as simple as
val makeModel = (make, model).parTupled
.flatMap { case (make, model) => validateMakeModel(make, model) }
# EitherT
The code using Validated
and Either
looked very similar so far. With Either
we were able to limit ourselves to
operations from various type classes, while with Validated
we had to use a very specific andThen
, but that might not
look like a compelling reason to prefer one over another. Imagine, however, that we want to make our validation
asynchronous. If we use Future
to represent asynchronicity, our code can look like this:
def validateMake(name: String): Future[EitherNec[String, Make]]
def validateModel(name: String): Future[EitherNec[String, Model]]
def validateDescription(text: String): Future[EitherNec[String, Description]]
def validateMakeModel(make: Make,
model: Model): Future[EitherNec[String, MakeModel]]
def validate(makeName: String,
modelName: String,
descriptionText: String): Future[EitherNec[String, Listing]] = {
val make = EitherT(validateMake(makeName))
val model = EitherT(validateModel(modelName))
val description = EitherT(validateDescription(descriptionText))
val makeModel = (make, model).parTupled
.flatMap { case (make, model) => EitherT(validateMakeModel(make, model)) }
(makeModel, description).parMapN(Listing).value
}
We are now converting Future[EitherNec[E, A]]
to EitherT[Future[_], NonEmptyChain[E], A]
by calling EitherT.apply
,
but everything else stays the same as in the synchronous version. This works because
EitherT[Future, NonEmptyChain[E], A]
also has an instance of the Parallel
type class. If you try to implement an
asynchronous version using Future[ValidatedNec[E, A]]
, the code will be much more complex.
# Summary
Validation doesn't have to be a special case of error handling that requires a different Validated
data type. You can
just use Either
, and then you don't need to decide for the callers whether they need error-accumulating behavior or
not — the monadic flatMap
will be available directly, and errors can be accumulated with the help of the Parallel
type class.