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.
One of the most straightforward ways in Scala to lift your error handling from raw exception throwing up to the typelevel is to use the standard library's Try
type. Try
is like Option
, if 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:
https://gist.github.com/3600d1cd3fa08ed3a492370e5cf6ae2b
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
type. Either
is the more traditional functional way of handling errors, and Try
is a nice compromise when you run in the JVM and Exception
s are everywhere.
Using 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:
https://gist.github.com/0ba7f31e5ae071dd77d3a9b01165e653
You get the point. Besides Either
and 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 Future
s. 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 SafeSerializable
with Option[A]
, Try[A]
, or 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: Try
and Id
. (Id
stands for identity and can be thought of as wrapping a value)
https://gist.github.com/f4fc6b0327f9b670cfb94139ace46cf7
Id[A]
is like a wrapper forA
that does not need to be unboxed - a handy trick.makeTry
returns an instance of SafeSerializable that wraps its results inTry
make
replaces our original make from last time by using theId
effect type
With our type class defined, let's re-define our convenient syntax for invoking its functionality.
https://gist.github.com/0b442cc86acd71cb4dc313693d63c889
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:
https://gist.github.com/4bd1ebcc779d219f00e17176eabda836
Time to try it out - let's start with a Try
-based serializer:
https://gist.github.com/c3292c9fed1a9292d099d5af8159d550
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 Id
-based serializer:
https://gist.github.com/344cd97cac1f7b6f7c8363c638fb1073
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.