A simple trick to improve type safety of your Scala code

Marcin Kubala
SoftwareMill Tech Blog
5 min readApr 29, 2020

--

In the recent TMWL post I wrote about an excellent Scala book Practical Functional Programming in Scala by Gabriel Volpe.

The book’s title contains “Functional Programming” and majority of the content might not be interesting for non-FP programmers but there are still a couple of tasty bites applicable to object-oriented programming.

In this post I’d like to show you how to harden type safety using only a couple of handy techniques, described in the first chapters of the book.

Let’s start with defining the problem.

Good old plain case class

I’m going to show you the refined types adoption path on a simple example of IM Message:

So, we have a case class representing a message, where the constructor takes two strings: recipient (user or channel) and message body:

So what’s the problem? Well, it’s easy to unintentionally mix the arguments and create an incorrect instance:

Improved version

This could be fixed either by using tagging:

or value classes:

Regardless of which way you prefer, it won’t be possible to swap the arguments anymore:

So far so good. But what about other scenarios, like an empty message body or a recipient that doesn’t follow the user or channel pattern?

Unfortunately none of the presented techniques allow to perform any kind of validation on the type level, so not only do we have to check the predicates at runtime but also remember about them! It’s possible to create an instance of String @@ Recipient from arbitrary value without performing any kind of validation…

And here’s where the Refined library for Scala comes to the rescue!

Refined types

In this step I’ll introduce the Refined library. It’s a port of a Haskell library of the same name, but it’s not bound to the functional programming ecosystem and I find it valuable for everyone who appreciates pair programming with the compiler, no matter if they prefer a functional or object oriented approach.

I’m going to define refined types for the recipient and body. Both are strings, so let’s start with just naming them:

So far we have poor type safety. Let’s add some constraints.

We know that both should be non-empty:

Note that both types are aliases for the same and you might have many aliases for non-empty strings that represent different kinds of values (like in the example above). I’m going to address this problem later — let’s focus on validation for now.

As I mentioned before, a recipient can be either a user (nickname preceded with @) or a channel (preceded with #). Let’s improve our code to reflect that:

Constraints can be combined using boolean operators such as And & Or.

NonEmpty has been replaced with StartsWith[_] and MinSize[)] because a valid identifier should contain the preceding user/channel marker and at least one character. The same goal could be achieved using MatchesRegex[_] but in this example I wanted to show you how to combine multiple guards.

Note for Scala < 2.13 users

Both new constraints take a parameter and if you are using Scala < 2.13 you will need to use shapeless’ witness (already provided by the refined library as the handy import eu.timepit.refined.W alias) in order to smuggle your literals to the type level:

This issue has been initially addressed in SIP-23 “Literal-based singleton types”

Here’s the final version of our refined types:

Let’s check if it works:

We are using automatic refinements and conversions here (import eu.timepit.refined.auto._), but in the real world values are going to be read from http requests, database or Kafka messages — later I’ll show you how the library guards type constraints for values that are not known at compile time.

Last three recipients are invalid and the compiler should fail with the following error messages:

As you can see the compile errors provide clear information about what’s wrong. That’s not so common 🙂

Also the compiler won’t let us pass Recipient in place of MessageBody:

In the last two lines you might spot another issue, which I’m going to address later in this article.

Attentive readers would ask

Okay, but 99% of data is not already known at compile time. How do refined types help?

Indeed it’s not possible for the compiler to predict what value will be eventually sent to our service, but at least it provides us a toolkit that enforces dealing with validation errors at the type level:

As you can see the return type of refineV enforces its users to handle the validation failure case.

Ok, but let’s assume that in our IM recipient names do not need to be preceded by something. We will end up with something we already saw at the beginning:

The problem is that both Recipient and MessageBody are simply aliases for the same type, which brings back the initial problem of mixed parameters!

In order to get rid of that issue we have to combine refined types with the solution from the previous step: tagging or value class. In this example I’ll use the latter:

Unfortunately this won’t compile because value classes may not wrap other user-defined value classes. In other words there is no way to use Refined type in a value class.

Are we forced to use plain case classes (with their runtime and memory overhead) or tagging instead? No!

Newtype

This is when another universal hint from the brilliant Practical FP in Scala book comes to the rescue!

Let’s meet scala-newtype — a library that provides wrappers for your values with no runtime overhead.

It’s as simple as adding a single annotation to our former value classes:

And it compiles!

Let’s check if this implementation still meets all the requirements:

And it does!

If you compare the final version with the snippet from the previous step (the one before we introduced refinements):

You will see that they are almost the same. Sometimes not so much is needed in order to dramatically increase type safety in your codebase!

“Refined Newtype” and Circe

Refined type with its powerful mechanism of validation seems to be a perfect fit for the DTOs, especially body requests. Most of the modern applications use Circe codecs to decode JSON.

In case of newtype objects we will need to provide a few implicit defs that lift Decoder[A]/Encoder[A] into Decoder[MyType]/Encoder[MyType], where MyType is defined as @newtype case class MyType(value: A).

I took the following code from Gabriel Volpe’s book:

.coerce[_] method is used to perform safe type casting between two types, useful when you have to extract underlying value from your newtype instance:

To add support for Refined types we will only have to add"io.circe" %% “circe-refined" % circeVersion to libraryDependencies and import all contents of the io.circe.refined package.

Codecs for our Message type should look like:

Now, we are ready to use them. Let’s encode and decode some message to see how it works:

As you can see in the last example, a valid JSON input that doesn’t meet constraints expressed in our types fails to decode.

Summary

In this blogpost I showed you how to combine Refined and Newtype libraries in order to gain a superb type safety. Integration with Circe requires some extra effort, but this is something you do only once in a project and probably Circe will support newtype out-of-the-box soon.

I hadn’t expected finding such a useful and universal technique in a book about strict functional programming.

All the code presented in this article can be found on GitHub: https://github.com/mkubala/refined-newtype-examples

--

--