Refined Types in Scala: the Good, the Bad and the Ugly
In this article I’ll talk about my trip with Refined Types in production environments, detailing both the highlights and caveats of this library. From a very simple approach I will be showing the problems I faced and how I solved them, showing how my final code looks like.
As usual, this article is not devoted to provide a full tutorial on Refined Types. For that sake, I can only recommend Miguel Ortega’s talk on ScalaMad (Scala users on Madrid, in Spanish) . If you are unlucky enough not to talk the greatest language on Earth, take a look at the official documentation or a more in-depth description by Methrat0n. Also, you need to understand what EitherT is, so see the official Cat’s documentation on it, which is great.
Intro
In all the above documents you can find examples on why the Refined Types are great. In our case, the initial reason for adoption was modeling something like:
Although it looks simple enough in the first place, there are in fact limitations being ignored: name and namespace could not be empty, and version had to be a positive integer.
With Refined Types, this can be modeled like this, and then use the refined type all over the business logic:
This is great because on our classes using this Schema we can do things like:
and we ensure that we’ll have a compilation error whenever we use a Name instead of a Namespace and viceversa. Most importantly, whoever reads this code in the future will have a clear vision on what we were trying to model.
Also, as this Name and Namespace are defined in a single point, they can be updated all over the code very easily. In particular, in our case the definition has evolved several times and now it looks like:
This means that our Domain is really clean, as any object being read/returned is checked very strictly to verify that represents exactly what we want. This greatly reduces the number of bugs and helps in the long term maintenance of the code base, as anybody reading this code can catch-up quickly, as it is trivial to understand how to use this functions.
Say goodbye to Isomorphisms, say hello to messy Either
A few posts back, I talked about how Isomorphisms really help you in having a clean and tested code. Well, forget about that. Things are about to get messy.
Using the previous example, let’s say that we want to model the logic that increases the version number by one (I know it doesn’t make much sense, it’s only for demonstration purposes):
- user enters a name and namespace of a schema
- we get the latest version of that schema
- we store it and return the value to the user
Before Refined Types, we could do something like
But now we need to convert from String to Name/Namespace before calling getLatestVersion. Also, we cannot just do “lastVersion +1” because it is not an Int anymore but a refined type, so we need to check that it is correct and convert it explicitly. And there is not an isomorphism anymore between UserInput + Version and Schema, because Schema uses refined types and UserInput uses Strings which may not be refined. Result looks:
So far, so good, it is a big ugly but still kind of manageable. An important thing to notice is that “version” and “result” are created by functions that do not return an Either, so we are taking them out of the for comprehension declaring them with “=” instead of “<-”
But imagine that getLatestVersion and store also return Either. Moreover, you have implemented a nice error hierarchy that looks like this:
And your code looks beautiful before Refined Types:
What happens when we throw in some Refined Types? Well, Either only works with one Left, which we want to be BusinessLogicError. We need to convert from the String that Refined gives to a BusinessLogicError. Result now looks like this:
we all agree that this is ugly and difficult to understand. We can however encapsulate the refined-related libraries in an implicit class:
and use it:
I really like how this looks. It is of course more verbose than the non-refined version, but we have very descriptive domain functions and we are ensuring that the input data follows certain restrictions.
EitherT!
Now, let’s say that the repository where we are storing schemas is a bit slow. We decide to implement the operations asynchronously using Future:
How does that look with Refined? Well, BAD, because we would need to use nested Monads like, Future[Either[Error, whatever]] (or Either[Error, Future[whatever]] and that is painful and makes Baby Jesus cry. What we are going to use instead is EitherT monad.
First, the Syntax class will be updated so it can wrap the result in any desired monad, not future but something a bit more generic. Note that we are using EitherT.fromEither function and it requires an implicit Applicative[F] so we are restricting the type of F to that. In plain english (kind of) this means: “We want to create a library as general as possible. EitherT[F, Error, Something] needs to know how to create a new Something. This is always done in a trait called Applicative. So we are limiting where our functions can be used to types implementing Applicative”
Also, when we use out getLatestVersion and store in our function we’ll have to convert from Future to EitherT too with a function called EitherT.right. Altogether, this looks like:
which is just a bit messy but kind of OK.
However, we saw before (refined_8.scala) that getLatestVersion and store could have issues, so we decided to return Either. As we are using Future now, I’ll make them return EitherT and the result is:
At this point, I think that the code looks good enough, is robust, asynchronous + nonblocking, and can be easily tested.
Testing with Refined Types
Generators
As some of you may know, I LOVE GENERATORS. They are the best, most underrated tool for testing that I’ve seen, and in my team at Twilio we put really a lot of effort into having good ones. But how does that mix with Refined Types?
Firs, if you use Refined Types, you need to build your own generators, and make sure that they are correct. What you do is to create a generator and then tell Scala “this generates stuff for my Refined Type, believe me”. In this example, that looks:
As you can see, there are no verifications that our generators are correct, what means bugs. We need then to check our generators. This is easily done with property testing:
It is important to note here that this is NOT verifying that your generators are good. For example, you could make the versionGen to just return “5” and this will pass the tests. So at this point you need to make some effort, not delegate everything on the compiler. Sorry for that.
Mocking
Good news: mocking this with ScalaMock is trival.
Bad news: mocking this with Mockito cannot be done.
So, use ScalaMock! This is an example of a simple test to show you the wirings and general approach. As you can see, first I am building the elements that will be managed by the mocks, then mocking the calls, and then calling the function to be tested and checking the result.
Tl; dr
Refined types are great, but they can make your code too complex. Here I present some tools to overcome that and have a clean, expressive domain with robust business logic and reliable tests