Use custom value classes for greater abstraction and type safety

Scala is one of our languages of choice. On our journey to Scala mastery, we continually pick up new tricks, which we soon put to use, if we find that they contribute to the quality of our codebase. This post is about one such trick: custom value classes.

What are value classes?

In Scala, classes that extend the AnyVal class are known as value classes. Notable examples of the latter include Double, Float, Long, Int, Char, Short, Byte, and Boolean, which correspond to Java's eight primitive types.

Custom value classes

By default, user-defined classes are run-of-the-mill reference classes, not value classes. However, Scala (from version 2.10 onwards) also allows you to define your own value classes.

A good use case for custom value classes

We, at Newsweaver, have found that the benefits of defining custom value classes greatly outweigh the costs. Defining custom value classes can be advantageous in many ways. An exhaustive list of use cases for extending AnyVal is beyond the scope of this post; I shall only focus on one use case that promotes greater abstraction and type safety.

Consider a domain dealing with people and accounts, in which each person and each account has a name and must be uniquely identified by a 32-bit integer. How should we model those IDs in our code? Let’s start simple and gradually refine our modelling approach...

The simplest and most obvious approach consists in representing both kinds of IDs by a plain Int:

final case class Person(id: Int, name: String)
val gavinId = 3
val gavin = Person(gavinId, "Gavin Belson")

final case class Account(id: Int, name: String)
val hooliId = 47
val hooli = Account(hooliId, "Hooli")

However, this approach has two major downsides:

  1. It suffers from what Kevlin Henney (one of my personal heroes) calls underabstraction. Writing code is more than aligning ones and zeros; it’s an act of communication. Here, nothing, aside from identifiers and potential comments, can convey to our readers that those Ints have a special meaning, that they really represent IDs.

  2. It lacks type safety: as far as the compiler is concerned, gavinId and hooliId are no different from any other Int value. Therefore, accidentally mixing up account IDs and person IDs is easy,

    val hooli = Account(gavinId, "Hooli") // happily compiles :(
    

    and so is misusing an ID as an undistinguished Int:

    val replicationFactor = 3
    val nonsense = hooliId + replicationFactor // happily compiles :(
    

    Debugging this type of programming error can be difficult, in particular because the compiler cannot help us, here.

Good: using custom wrapper classes

One improvement involves defining custom PersonId and AccountId classes that wrap around an Int,

final case class PersonId(value: Int)
final case class AccountId(value: Int)

and redefining our Person and Account classes accordingly:

final case class Person(id: PersonId, name: String)
final case class Account(id: AccountId, name: String)

This is much better than using plain Ints. These custom ID types convey intent much more effectively than identifiers and comments alone would. They also provide strong compile-time guarantees that prevent clients from misusing them:

val gavinId = PersonId(3)  
val hooli = Account(gavinId, "Hooli") // does not compile :)  

Unfortunately, by wrapping an integer value in a class, we have added one level of indirection, thereby trading abstraction and type safety for performance.

Better: using custom value classes

The solution to this performance problem is to define PersonId and AccountId as value classes:

final case class PersonId(value: Int) extends AnyVal
final case class AccountId(value: Int) extends AnyVal

As a result, all PersonId and AccountId values are simply represented as Ints at run time. The simple act of extending AnyVal greatly reduces (if not eliminates) any performance penalty associated with using custom wrapper classes.

The fine print about value classes

Scala places a number of restrictions on value classes. In particular, a value class

  • must have exactly one val parameter;
  • can define methods, but no val or var fields, nested traits, classes or objects;
  • cannot be extended.

Moreover, because the JVM does not natively support value classes (although it may in a future release), allocation of runtime objects is actually required, in some cases.

Summary

Custom value classes afford greater abstraction and correctness, without significantly compromising performance. Use them liberally in your code.

More about Scala from Newsweaver

Pierre wrote an entry in our tech blog last year about how we promote a functional style in our Scala codebase, and Tom and Donnchadh recently gave a couple of talks, at CorkDev and CorkJUG, in which they extolled the virtues of Scala.

Julien Cretel

Software Engineer

Cork, Ireland

Subscribe to Newsweaver Technology Blog

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!