It’s quite a common scenario to work with external API’s data, that is sometimes invalid. As developers we want to keep our code DRY(Don’t Repeat Yourself) and specific to our domain.
For example, we get a requirement to support a registration form, that consists of username and password. If they are valid, they should be part of some sort of computation in our system. In case of failure, we would like to know what exactly failed in the computation, was it the email, password or some other invalid computation.
Let’s say our initial setup looks like this(commented out code is relevant if you want to run our code using Ammonite REPL):
The validateUserVersion0 implementation above is not great, but it lets check if UserDTO is valid and throws an exception if it’s not, but exceptions are slow. So can we do something better?
As Functional Programmers, we have a concept called smart constructors. So let’s try using that:
So with the validateUserVersion1, we validate if email and password are valid and then inform the client of the code, that this validation might fail. If it fails we return None(Option can be either Some(value) or None).
We are now aware of an error, but if it’s part of some bigger computation, how do we know it’s the User Validation that failed?
With validateUserVersion2 we express the result as Either(Either can be either Right(correct) or Left(error) case). So now, when using our new validation method in another computation we can leverage the information that the user validation failed.
What about the case when we care what exactly failed?
With validateUserVersion3 we can express the failure of email, password or both. That’s great, but the price we paid was clarity of syntax. What if we have 3 or more fields? The number of possible error combination makes the code really verbose. Let’s try making the code more concise.
So by using for comprehension in validateUserVersion4, we reduced the code a lot, plus adding additional properties does not seem so difficult. Alas, our errors cases now contain only single failing case, for example, if email and password are invalid we would only be informed about the email. How can we keep the syntax concise and let the errors aggregate?
By using mapN together with Validated Type in validateUserVersion5 we are able to represent errors that accumulate if a failure occurs.
Using Strings as Errors only works for Toy examples, what if we expect our errors to be domain specific and we want to add checks that depend on other checks passing? Let’s start by defining our domain errors:
Let’s say there is a list of blacklisted user emails we consider invalid.
With validateUserVersion6 we expressed that we validate our Email andThen validate if it’s part of BlackListed users. So now if user email is BlackListed we only get BlackListedUserError instead of InvalidEmailError and BlackListedUserError. As you can see we also introduced NonEmptyList, so we can express that you can not have Invalid State without an Error.
As a side note Validated also supports transformations to Option and Either. So we can represent previous versions as well:
I will leave you with an exercise to try to abstract our current implementation to work with any higher kind type, so the client can decide what type of errors he/she wants. While solving this you might be interested in looking into ApplicativeError and MonadError.
It’s quite common to expect your data to be valid when you’re working with it in your domain context. This can be easily achieved by using a combination of Validated, Either and Option. In case you care about the aggregation of errors, you can use Validated. In cases your errors depend on the previous check you can use Either, and in cases when you are only interested in the data validity, you can use Option.
- This post was inspired by https://www.youtube.com/watch?v=P8nGAo3Jp-Q
- Github repository containing demo code https://github.com/Algiras/Data-Validation-With-Cats
- Binder Link to Try out the code online: https://mybinder.org/v2/gh/Algiras/Data-Validation-With-Cats/master?urlpath=lab%2Ftree%2Fnotebooks%2Findex.ipynb