Wtf is Refined ?

Methrat0n
6 min readFeb 8, 2019

--

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 Refined.

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 apply and unapply definitions along with from and 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.

Because 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 from and unsafeFrom functions, they are meant to wrap a value into your refined type.

The 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.

Simpler validations

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.

id and code are used to build instances of Id and 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 Id and 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.

Easier testing

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

Refined use 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.

The 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.

Conclusion

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.

--

--