One can tell a story about validation in Scala from different angles. One is to build up a motivation by describing the
theoretical foundations like Applicatives
and Monads
another option is to start practical, focus on a domain and see
where things go from there. The former angle has been a topic of various
blog posts in the past, so I try the other way and start of with
a little domain not that unrelated to the ones you may find in your Scala programs:
This is the domain of tradeyourmusican.com, where bands can search for musicians matching their profile. The musician is at the core of this domain model which makes its quality a business concern. Validation is a process which we use to ensure data quality and give feedback to users about the interactions with our applications. What would be the simplest way to give feedback about a wrong interaction? Lets take the birthdate as an example. We want to make sure that the musicians on our site are at least 16 years old.
A simple way to do that would be to use the Option
type in Scala. This type can either be Some(value)
or a None
.
Given the restrictions of Option
the only way to model our age validation would look like this:
Combined with other rules a complete validation with Option
might look like this:
If the musician does not match our expectation we get a None
. While this can work for very simple cases it has the
disadvantage that None
does not really tell you much about the context of the error and this also limits the value of
the error message you can present to your users.
The classical Java way of doing this would be to use exceptions to give feedback about what happened exactly. In Scala syntax this approach would look like this:
This approach gives you a way to communicate the cause of an validation error but the return type Unit
leads to
bleeding in the eyes of a functional programmer ;). But what are the practical implications of this design? In order to
interact with the results of the validation you have to work with try/catch or your application will fail. In contrast
to Java, exceptions in Scala are not part of a functions contract, which makes it easy to forget about them.
A way to get the best of both worlds is to use scala.util.Try
which is a similar to Option
in a way that it has
two states. Success
like Some
encodes a value if all steps were successful. The failure type on the other hand is a
very different beast compared to None
. Failure
takes a exception as a value which gives Try
the possibility to use
this as error communication channel. This is similar to throwing exceptions, with the difference that the validation
result is a value which can be composed, passed around and processed much like Option
. Given those similarities it is
not a surprise that the validation looks very similar to one using Option
:
Very related to Try
is the Either
data type, which is part of the standard library. It also has two states,
with the difference that you can choose to use a type of you choice to represent an error. Since Either
is
not bound to validation scenarios it does not even prescribe which side has to be used to hold the error.
Now we have seen four options how to do validation in Scala, each having its own merits but if you take a look what you can do with those abstraction you soon going to find out that all of them are not suited for a wide range of validation scenarios. Why is that the case? Lets take a look at a sample validation:
Have you noticed that we got only one error, even if we would expect another error for the missing instruments? The data
type driven abstractions we have seen so far implement a so called fail-fast behavior. With other words we compose
Option
/ Try
or Either
in a way that we assume that the validation was successful, but as soon as we hit the first
error, the validation chain stops and the result is fixed to the result of that error.
The reason why thats not the preferred approach in many scenarios are modern user interfaces including APIs. For those
you want to get as much feedback as possible given a user interaction. Thats why we need additional data types with a
collecting validation behavior. The most prominent data type of this kind in the Scala ecosystem is
scalaz.Validation
. Like Either
you are free to choose which type you want to use for representing your errors. For
the sake of simplicity we start of with a String
as error type. Because we do not want to repeat this decision for
every validation, we define a type alias like this:
If we integrate this type into our validation scenario we get the following:
While the syntax of the single validations it not that surprising you may wonder about the syntax of the validation
composition. Since scalaz.Validation
collects errors, it can not rely on the same composition functions used by data
types like Option
. The function at the end of the composition with |@|
gets called when all validations were
successful. This function can be used to transform the result of individual validations into a new datatype. In this
example I just ignore the parameters and return the musician we got as input paramter.
When we evaluate the validations based on scalaz.Validation
we get this:
While the success case is pretty normal the failure case gives one concatenated string resulting form two individual
errors. While this shows that validation collection works, it is not really practical to have them in this format. What
we want instead is a list of errors, which we can use to display errors individually. Fortunately scalaz
already
defines a type alias for that use case:
Instead of a simple scala.collection.List
the scalaz.NonEmptyList
type was chosen since the Failure
case of a
validation only gets selected if at least one error is present. Thats why scalaz.NonEmptyList
is the natural error
type. Lets see how the result looks like:
Nice! I guess thats it for the introduction. Next time I will show you an
extension to scalaz.Validation
which aims to make data type based validation
as practical as annotation driven Java tools like Hibernate Validatior.