Typeful error handling with higher-kinded types
Previously we looked at a type class which we called
SafeSerializable. We used it as a type-safe, composable way to serialize our data types. We closed with an observation that, while our happy path is quite type-safe, our error handling is not. Currently there is nothing about the type signatures of our serialize and deserialize methods that indicate that things could go wrong. Let's explore the options for changing that.
Returning a Try
One of the most straightforward ways in Scala to lift your error handling from raw exception throwing up to the type level is to use the standard library’s
Try is like
None carried an exception. By returning a
Try, we inform our caller that an error may have occurred, and they need to deal with it. Here is how this would look:
Now our callers will need acknowledge and handle the possibility of an error in order to get to the successful result. They could of course call
.get on the resulting
Try, but then we've still forced an explicit decision to ignore possible errors, which is still a win.
One shortcoming of
Try is that, when you have a failure, you have to put an exception inside. If you instead have some actual error types, and would rather return them without raising an
Exception, there is also the
Either is the more traditional functional way of handling errors, and
Try is a nice compromise when you run in the JVM and
Exceptions are everywhere.
Either with an explicit error type has the advantage of making it much easier to pattern match on all possible errors; this is less straightforward when all you have is a
Throwable. Here is what this might look like:
You get the point. Besides
Try, we could also decide that serialization is taking a very long time, and that we don't want to block, and start returning our results inside
Futures. Maybe you love
cats.Validated (it is pretty great) and would prefer to use it here instead of
Either. In fact, there are many types in Scala that one might want to choose from to represent the possible 'effects' of serialization.
Instead of choosing just one, or worse, implementing 10 copies of
SafeSerializable with a different effect type each time, let's see if we can make our type class work with all effect types.
The thing that all of these ‘effect types’ have in common is that they take a type parameter which represents the type of value they contain (or may contain). You can think of them as containers or wrappers. When we want a ‘type that takes another type’ in a function, we use the syntax
F[_]. This is actually a higher-kinded type parameter. What we're saying is pretty simple, though: you can implement
Future[A] - it doesn't really matter as long as the effect type you give takes a single type parameter.
Here is our new type class, now generalized, with convenience constructors for two effect types:
Id stands for identity and can be thought of as wrapping a value)
Id[A]is like a wrapper for
Athat does not need to be unboxed - a handy trick.
makeTryreturns an instance of SafeSerializable that wraps its results in
makereplaces our original make from last time by using the
With our type class defined, let’s re-define our convenient syntax for invoking its functionality.
Note that this time, serialize and deserialize take an additional type parameter: the same
F[_]. They must know about
F since it is their new return type!
Now we’re ready to handle some serialization errors. Let’s set up our test class again with a basic serialize and deserialize function:
Time to try it out — let’s start with a
We were forced to pattern match on
result to get at our bytes, handling possible errors in the process. However, if we wanted to pretend we had our old, dangerous version of
SafeSerializable back, we'd just use an
And there you have it. We’re returning error-aware types now from our serialize and deserialize calls. We’ve significantly increased the generality of our type class by allowing it to return a ‘wrapped’ value, or an effect type.
We haven’t coupled
SafeSerializable to any particular style of error or effect handling, we've simply provided an interface to which a consumer could 'plug in' their own effect types.
The technique of abstracting over an effect type or monad has been called “Finally Tagless Encoding”. I find the term confusing and awkward so I didn’t use it here, but for a deeper dive into the concept, that’s the search term to use! In particular, this post by Adam Warski is excellent for familiarizing yourself with these types of patterns, and working with monads in general.
Originally published at gist.github.com.