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
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 Poppulo, 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...
Naïve: using plain
The simplest and most obvious approach consists in representing both kinds of IDs by a plain
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:
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.
It lacks type safety: as far as the compiler is concerned,
hooliIdare no different from any other
Intvalue. 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
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
AccountId classes that wrap around an
final case class PersonId(value: Int) final case class AccountId(value: Int)
and redefining our
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
AccountId as value classes:
final case class PersonId(value: Int) extends AnyVal final case class AccountId(value: Int) extends AnyVal
As a result, all
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
- can define methods, but no
varfields, nested traits, classes or
- cannot be extended.
Custom value classes afford greater abstraction and correctness, without significantly compromising performance. Use them liberally in your code.
More about Scala from Poppulo
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.