Any application sooner or later will fail. Imperative style programming usually handles this using side-effects by propagating exceptions and handling them later on. This approach introduces statefulness and deferring the error to outer bounds of the application. This creates hidden control-flow paths, that are difficult to reason about and debug properly when the code grows too much.
The functional and side-effect free approach that resolves this problem, is wrapping the result of the computation in a datatype, that will represent the possibility of the computation failing. Of course this also means that anything using this kind of value with a failure context needs to take into account the possibility that the result represents a failure and act accordingly.
Scala already has data types, that we can use to represent failure in such a manner – Option when the value might not exist, Try when computation might fail with exception and Either to signal that two distinct types can be used. The most “general” one is Either and thus we will use it to show the concept explained beforehand.
Either introduces two container types:
Right, each capable of storing different type of value. It is often used for returning results, that could represent either successful computation or failure message. For example the
Left value can be of some type that will represent an erroneous result of the computation. The
Right value represents the successful result type, e.g.:
In this simple example we handle the possible failed state of the computation (division by 0) by simply returning a String that describes the error. Of course, a better idea would be using some kind of algebraic structure that would represent all of the possible erroneous states, instead of processing arbitrary strings. We will do exactly that in one of the future examples where we will reimplement the above function using
If you are into functional programming in Scala you probably already heard about (or used) Scalaz and it’s successor – Cats. Both of them include types specifically designed for handling with errors similarly to the one presented in the above example.
Those types could be taken as more specialized extensions of
Either designed for failure handling, in fact they are isomorphic (one can be transformed into the other and vice versa). In the following part of the post I would like to show you the data types provided by the Cats library that replace Either for handling failure. I will explain how to use them and what is the main difference between each of them.
Xor is almost the same as Either (they’re isomorphic, i.e. one can be freely transformed into the other) but there are two important differences that make Xor a better solution in most cases:
- Xor is a Monad (simply put in a practical context it has a
- It’s right-side biased by default, i.e. the
Rightvalue of Xor represents success. This produces some differences on how some functions work when using it (e.g.
mapwill only apply to the right-sided value/type)
Xor is an algebraic datatype represented in the following way (simplified):
Xor having a flatMap enables us to use it in for-comprehensions. When used in this fashion Xor will “short-circuit” the computation whenever it encounters a failure (a
In the following example, I will try to show how we can use Xor to represent possible failure in a similar fashion as with
Either and additionally using it to stop a computation represented by a for-comprehension, when it encounters a failure (represented by
Left). So here it is:
|+| Cats operator represents the
combine function, that must exist for any datatype of the Semigroup type class. For the right-sided type
Double it simply means adding two numbers and the evidence for
Double being a
Semigroup is delivered by Cats. If the value is
Left then it will return the same
The result of this computation will be
Left(DivisionByZero). The third division
divide(3.0, 3.0) will be omitted because
divide(1.0, 0.0)resulted in a
Left. If there wouldn’t be any failures the result would simply add up the results of the divisions.
As we said before Xor has a right-side biased
map function. We can use it to transform the successful result stored in the
Right value, e.g. using the last function we created:
The second datatype provided to us by Cats for failure handling is called
Validated. It allows us for overall validation, which is impossible when using
Xor as only the very first failure is propagated as the result when chaining them using
This difference arises from the fact that
Validated is not a Monad, which implies there is no
flatMap function for it. This means You can’t use it within a for-comprehension because it doesn’t have any concept of “failure” as it is designed to accumulate all of the errors.
Validated contexts using the fact that it is an Applicative, that basically tells us how to handle functions that are inside the
Validatedcontext and apply them onto values inside other
The algebraic datatype
Validated is represented in the following way:
The type held in the
Invalid value should be of the
Semigroup type class, so that we can combine errors at each step. This is usually done by using the list type, i.e.:
Semigroup evidence for
List is supplied here by the standards Cats library.
Usually, when the value is
Invalid and stored in a
List we want to ensure that it is non-empty. This is pretty obvious as we want something stored in it that will describe what error/failure occurred. This can be ensured by using the Cats
NonEmptyList type. It ensures that the
List that it wraps around will always have at least one element.
Validated can be used together and, in fact, that combination is very common. Thus Cats provides us already with an type alias along with helpful functions that we can use to operate on this kind of construct:
Well this basically explains what is
Validated and the logic behind it. Here I would like to show a very basic example of using it for creating an object holding validated user data. Checking the password length and email pattern are done independently. The code:
So basically we apply the validation helper function onto the
User constructor function. The resulting
Validated instance will either hold a valid
Userinstance or a non-empty list of errors that occurred during the validation phase (password too short, e-mail pattern was wrong or both).
So we are at the end of this basic introduction into handling failures with Cats and the benefits that it presents. This by no means exhausts all the information on the topic.
There is a plethora of helper functions provided for both types, e.g. for transforming from the standard
Try types into
Validated and the other way around. I have provided useful links at the end for further reading about the subject.
Thanks for reading and I hope for Your success in using the knowledge learned here!
- Easing into functional error handling in Scala
- Cats Xor documentation
- Cats Validated documentation
- herding cats – Xor
- herding cats – Validated