Goal

In this article we’ll learn how to use the Scala type system for avoiding runtime errors. On our way we’ll learn some basics about scala-cli.

Bootstrap

In this article we’ll rely on simple examples, thus instead of sbt we’ll use scala-cli, a lightweight command-line utility to work with Scala.

First we’ll need to install the utility. For that visit https://scala-cli.virtuslab.org/install and select the method of your preference.

Once that’s done you should have scala-cli command available. It will allow you to easily run Scala programs and scripts. I encourage you to visit the getting started section of the documentation, especially the Project approach.

Unsafe approach

Let’s start with an example that doesn’t benefit from the type system. Imagine we are modelling an order for a store. Order would consist of an id and a list of OrderLine - pairs of product id and it’s quantity.

Reasonable product line would not allow for an empty product name as well as keep the quantity as a positive integer. The following implementation based purely on the standard library achieves those goals.

//> using scala "2"

case class UnsafeOrderLine(product: String, quantity: Int)
object UnsafeOrderLine {
  def safeApply(product: String, quantity: Int): UnsafeOrderLine = {
    if (product.isEmpty())
      throw new RuntimeException("Product is empty")
    else if (quantity <= 0)
      throw new RuntimeException("Quantity lower than 1")
    else
      UnsafeOrderLine(product, quantity)
  }

}

// Works fine!
println(UnsafeOrderLine.safeApply("123", 10))
// Throws runtime exception 👇
UnsafeOrderLine.safeApply("", 10)

The code is also available as a gist on https://gist.github.com/majk-p/4845b8efadaab3c069795d0a587a77f3.

This code works and does bring the necessary guarantees about the fields, but it does so in runtime. This means we should now write some tests for this logic and maintain it.

Since you already have scala-cli available we can execute the gist directly in your console using like this:

scala-cli https://gist.github.com/majk-p/4845b8efadaab3c069795d0a587a77f3

And the output looks like this

UnsafeOrderLine(123,10)
Exception in thread "main" java.lang.ExceptionInInitializerError
        at unsafeorderline_sc$.main(4845b8efadaab3c069795d0a587a77f3-master/unsafeorderline.sc:36)
        at unsafeorderline_sc.main(4845b8efadaab3c069795d0a587a77f3-master/unsafeorderline.sc)
Caused by: java.lang.RuntimeException: Product is empty
        at unsafeorderline$UnsafeOrderLine$.safeApply(4845b8efadaab3c069795d0a587a77f3-master/unsafeorderline.sc:7)
        at unsafeorderline$.<clinit>(4845b8efadaab3c069795d0a587a77f3-master/unsafeorderline.sc:19)
        ... 2 more

As we can see the code did not allow us to create an invalid order line. With Scala’s type system we can do much better, let’s use the type system for our benefit.

Refined

This is where we’ll start using refined, a library that provides rich types and type level predicates to offset runtime checks to the compiler. To start with let’s try implementing OrderLine in terms of types provided by refined.

//> using scala "2"
//> using lib "eu.timepit::refined:0.10.1"

import eu.timepit.refined.auto._
import eu.timepit.refined.types.string._
import eu.timepit.refined.types.numeric._

case class OrderLine(product: NonEmptyString, quantity: PosInt)

OrderLine("100", 10)
OrderLine("101", 5)

It’s available on https://gist.github.com/majk-p/8745999480b763ff17ff4c2f5d89ce75

Running the code

scala-cli https://gist.github.com/majk-p/8745999480b763ff17ff4c2f5d89ce75

Try playing with the example and changing the product to an empty string, or providing non-positive quantity.

There’s no doubt that the code is much simpler, and since there’s no explicit logic we don’t have to write any tests.

Now let’s move forward and implement the whole Order class as well. To benefit more from the demonstration, let’s assume that the orderId has to be a valid UUID and the order lines cannot be empty.

For UUID we’ll reach out to eu.timepit.refined.api.Refined and load the UUID refinement from eu.timepit.refined.string. To make things more interesting, the guarantee of non-emptiness for order lines will be provided by the data type cats.data.NonEmptyList from cats.

//> using scala "2"
//> using lib "org.typelevel::cats-core:2.8.0"
//> using lib "eu.timepit::refined:0.10.1"

import cats.data.NonEmptyList

import eu.timepit.refined.auto._
import eu.timepit.refined.api.Refined
import eu.timepit.refined.string._
import eu.timepit.refined.types.string._
import eu.timepit.refined.types.numeric._

case class OrderLine(product: NonEmptyString, quantity: PosInt)

case class Order(orderId: String Refined Uuid, lines: NonEmptyList[OrderLine])

val orderLines = NonEmptyList.of(
  OrderLine("100", 10),
  OrderLine("101", 5)
)

val order = Order(
  "c93dd655-caca-4c99-aad8-3d91ad7a1b7e",
  orderLines
)

println(order)

One new thing to focus on is the String Refined Uuid construct. This one in particular means that we want to enrich the regular Scala String type with a Uuid predicate.

If you’re not familiar with the type notation, it’s the same as Refined[String, Uuid]. It’s not specific to the Refined api. Since we have scala-cli at hand, let’s try it with Either.

$ scala-cli repl --scala-version 2
Welcome to Scala 2.13.8 (OpenJDK 64-Bit Server VM, Java 11.0.11).
Type in expressions for evaluation. Or try :help.
    
scala> def x: Either[Int, String] = ???
def x: Either[Int,String]

scala> def x: Int Either String = ???
def x: Either[Int,String]

Back to our example, let’s execute the code:

Compiling project (Scala 2.13.8, JVM)
Compiled project (Scala 2.13.8, JVM)
Order(c93dd655-caca-4c99-aad8-3d91ad7a1b7e,NonEmptyList(OrderLine(100,10), OrderLine(101,5)))

Once again please be invited to play around with the values, see what compiles and what doesn’t.

Serialization

Now that we have the data modelled decently, let’s try adding JSON codecs. For that we’ll use the circe library and it’s circe-refined integration.

Let’s start with importing all the necessary stuff

import io.circe.Codec // Codec type 
import io.circe.generic.semiauto._ // Derivation of codecs
import io.circe.refined._ // Derivations for refined types
import io.circe.syntax._ // for `.toJson` syntax
import io.circe.parser._ // for parsing json

Then we’ll need to create instances of codecs. Here’s related part in the circe documentation. In this example let’s be explicit and derive the codecs for both our OrderLine and Order.

object OrderLine {
  implicit val codec: Codec[OrderLine] = deriveCodec
}

object Order {
  implicit val codec: Codec[Order] = deriveCodec
}

We needed to implement codecs for both, because when we want to serialize an Order, traversing down the fields one of the types we need to serialize would be the OrderLine.

Last thing we’d like to test if the decoder really helps us with verifying the data correctness. Let’s check it by providing both valid and invalid data:

val json = """
{
  "orderId" : "c93dd655-caca-4c99-aad8-3d91ad7a1b7e",
  "lines" : [
    {
      "product" : "100",
      "quantity" : 10
    },
    {
      "product" : "101",
      "quantity" : 5
    }
  ]
}
"""
println(decode[Order](json))


val invalidJson = """
{
  "orderId" : "not-an-uuid",
  "lines" : [
    {
      "product" : "100",
      "quantity" : 10
    },
    {
      "product" : "101",
      "quantity" : 5
    }
  ]
}
"""
println(decode[Order](invalidJson))

After running the test we can see it works very well

Right(Order(c93dd655-caca-4c99-aad8-3d91ad7a1b7e,NonEmptyList(OrderLine(100,10), OrderLine(101,5))))
Left(DecodingFailure(Uuid predicate failed: Invalid UUID string: not-an-uuid, List(DownField(orderId))))

And just for the completeness, here’s the full file.

//> using scala "2"
//> using lib "org.typelevel::cats-core:2.8.0"
//> using lib "eu.timepit::refined:0.10.1"
//> using lib "io.circe::circe-core:0.14.2"
//> using lib "io.circe::circe-parser:0.14.2"
//> using lib "io.circe::circe-generic:0.14.2"
//> using lib "io.circe::circe-refined:0.14.2"

import cats.data.NonEmptyList

import eu.timepit.refined.auto._
import eu.timepit.refined.api.Refined
import eu.timepit.refined.string._
import eu.timepit.refined.types.string._
import eu.timepit.refined.types.numeric._

import io.circe.generic.semiauto._
import io.circe.Codec
import io.circe.refined._
import io.circe.syntax._
import io.circe.parser._

case class OrderLine(product: NonEmptyString, quantity: PosInt)

object OrderLine {
  implicit val codec: Codec[OrderLine] = deriveCodec
}

case class Order(orderId: String Refined Uuid, lines: NonEmptyList[OrderLine])

object Order {
  implicit val codec: Codec[Order] = deriveCodec
}

val orderLines = NonEmptyList.of(
  OrderLine("100", 10),
  OrderLine("101", 5)
)

val order = Order(
  "c93dd655-caca-4c99-aad8-3d91ad7a1b7e",
  orderLines
)

println(order.asJson.toString)

val json = """
{
  "orderId" : "c93dd655-caca-4c99-aad8-3d91ad7a1b7e",
  "lines" : [
    {
      "product" : "100",
      "quantity" : 10
    },
    {
      "product" : "101",
      "quantity" : 5
    }
  ]
}
"""
println(decode[Order](json))


val invalidJson = """
{
  "orderId" : "not-an-uuid",
  "lines" : [
    {
      "product" : "100",
      "quantity" : 10
    },
    {
      "product" : "101",
      "quantity" : 5
    }
  ]
}
"""
println(decode[Order](invalidJson))

As always the code is also here: https://gist.github.com/majk-p/bf65be99efcf897d071c4ab6a030d13a and again I encourage you play with those examples on your own by manipulating the data and exploring the apis.

Summary

In this article we have learned something about runtime errors and some techniques of moving the constraints to the compilation layer. Apart of the code examples from this article, feel invited to explore more possibilities provided by the great tool of scala-cli.