Wtf is Refined ?
Since I published my article Reduce the domain of your types with Refined I received some thank’s and a lot of questions of the “wtf is this library” kind. In this article I aim to answer these questions and to help you understand the benefits and drawbacks of
So, what is Refined ?
Refined is a Scala library, a port of an
Haskell library. It aims at helping developers constrain types.
In the above example I defined a
PositiveInt type. Its possible values range from 0 to
Int.MaxValue. Now, if I want a function to take a positive integer as its input, I can use the newly created
PositiveInt type. Therefore, I ensure that the value it receives has already been validated at some point. So, I don’t need to verify the correctness of the parameters anymore.
How to define a great refined type
Refining types is not enough. You also need to define the companion object of your type.
By inheriting from
RefinedTypeOps all the usual functions are defined for our type. We get
unapply definitions along with
unsafeFrom . The
unapply function behave simply and let us access the inner value of the refined type, but the
apply function is a bit more complex. It will only work with literal values which are values known at compile time.
positiveInt value (first line) is known at compile time, the
apply function hold. On the other hand, the
int value is a reference, meaning it’s not possible to check it’s value at compile time.
Under the hood (you need to know how macros work)
The apply function call a macro, transforming the Scala code at the call site. The
Expr instance taken by the macro contains only the parameter of the
apply call, no more information. If this
Expr does not contain an instance of
Literal its impossible (with the current
Scala macro implementation) to check what the parameter hold, even if its value have been hard-coded somewhere else.
As for the
unsafeFrom functions, they are meant to wrap a value into your refined type.
from function validate the input data and then return an
Either for you to compose with. The
unsafeFrom also validate the input data but will throw an
IllegalArgumentException if the validation failed.
Refined can simplify your validations while also making them safer. Instead of writing custom ones, I recommend you refine your types and simply use the build-in validator.
code are used to build instances of
CodeArticle using the
from function. Because this function returns an
Either[String, RefineType] we can chain this call in a for-comprehension. Mapping the error messages to your application error system is quite trivial.
But maybe you dont want your validation to stop at the first one ? For this you can use a
Validated from the cats library.
Business Rules in the Type Systems
As seen with
CodeArticle the refined types can have business value. In other words,
Refined let you engrave business into the Type System.
These examples are from a real production project
The advantage of having your business model in the Type System is, before all, clarity.
To me the first definition seems clearer, as I can very simply find what a
PhoneNumber is. Using the same words inside the code as in project talks greatly decreases the complexity. As a side effect, it’s much more simpler for newcomers to learn about the business just by reading code.
Advanced Usages (you can skip this):
By having the validation inside the type system one can derive a lot of boilerplate from it. For example, we used it to derive the Json-Schema of our types, which gave birth to the Restruct library.
Testing is writing in-code specs. ScalaTest promote the use of Specs at the end of the tests classes names for this very same reason.
Refined enforce business rules in the Type System, therefore reducing the range of values a type can take. The more precise a function definition is, the less test it needs.
As you can see, we firstly capture the good execution of the function and lastly describe a precise behavior. The second test wont even compile as the siret value has the wrong format.
Putting it simply, a function can be used in two contexts : before input data is validated or after. As validation is a side-effect, functions working with non-valid data are rare, and the other group is the majority. For this majority, inputs should be strictly typed. It avoid protective programming (no
IllegalArgumentException ) and transform the tests of this function into specs, as we can describe the nominal behavior of the function alone. Tests then become references we can trust.
Drawbacks — Base Type
For now I have talked a lot about the advantages of
Refined but nothing is perfect.
Refined value will not behave like the base type they were made from. This is an in-progress work and the next release should contain a fix for this. Still, for now it’s possible to use refined values as base types in function calls only.
While this work fine, it is cumbersome and is error prone for developers new to
Refined . To avoid this behavior here is my personal solution :
You will need to import
RefinedOps._ every time a refined value should be converted to the base type.
Note : This part will be updated when a new version is available.
DrawBacks — Compilation Time
Shapeless inside and a lot of it’s own macros, which directly impact the compilation time. This can become a problem for large projects, but solutions exist. The simpler one is to split a project into smaller, multiple ones. The compilation will only happen on the project you modified.
It may sound like a huge change to support a simple library, but this is a problem only for really large projects. For other reasons, which are not in the scope of this article, I would argue that this projects should already have been split into smaller ones.
Still, tests are a place where we will use a lot of values.
The three values here are wrap at compile time thanks to the
auto._ import. If you end up creating hundreds, the compilation cost will be high, in the order of twenty seconds for every hundred.
This can be overcome by calling
unsafeFrom by hand.
This way no compilation time overhead. Nevertheless, it comes at a runtime cost, which is way lower.
Finaly, if the runtime overhead is still too much, another function can be called.
Beware, this is basically an
asInstanceOf . The
unsafeApply function does not check the value you pass it, possibly creating an error in your input.
DrawBacks — Singleton Types
As you have seen in the examples of this whole article,
Refined is a good match with Singleton Types. A type is called singleton if reduced to a value.
Id type is built using two singletons : 100 and 500. They are types, you can see how I use them.
If this does not seems like simple Scala to you, that’s perfectly normal. Singleton types were compiler internals until Scala 2.13. If you need to support a lower language version, you can depends on Typelevel Scala. This fork of the Scala compiler support singleton types since Scala 2.11
The other possibility, without switching compiler, is to use the Shapeless notation, which support Scala 2.10.
As you can see, Shapeless notation is harder to read. Still, it’s the recommended way in
Refined examples for now as in Scala 2.12 Singleton Types were not usable.
Refined is a library to reduce the values a type can take (type refinement). It can be of help to enforce business rules in the TypeLevel, giving you power over functions by declaring clearer parameters. This way, you make sure values are valid in every functions and nothing can have break, simplifying both, validation and testing. Validation can be made in one unique place, and
Refined offer built-in functions. Testing only need to care about business cases, not technical ones, as your functions inputs will necessary be valid.
This come at some cost, which can be mostly avoided. It may add boilerplate for now, but a workaround exist and the library will soon be patched. Compilation time can become a burden, especially tests compilation. This can be avoided by calling the right function and avoid auto-wrapping at compile time. Lastly, it’s much clearer with Singleton Types but they were not part of Scala before 2.13. The alternatives are the TypeLevel compiler or the Shapeless Notation.