Value Classes in Scala
Type systems and compile-time type checking are great things that can save you a couple of hours of debugging and also have documenting potential, could make the code more understandable. In my opinion, it’s wise to use them, and unfortunately, sometimes we don’t do this enough. Consider Integer
/Int
/int
. A counter could be Integer
, an entity identifier could be Integer
, an integer number in arithmetic expression could be Integer
. In most cases all this Integer
s have nothing to do with each other: in your domain it is a bad idea to compare them, do arithmetic operations on them, pass one instead of another as a function parameter etc.
In one of my projects (in C#) there are a dozen of domain entities that have integer identifiers that are passed all over the code. After a couple of bugs connected with mixed up identifiers of different entities I’ve solved this problem by replacing plain integer numbers with struct
s (in C#, struct
s are value types used for representing lightweight objects such as Point
or Color
) like Id<EntityName>T
(T
is to distinct type from property names). The key idea was to introduce a new level of types to let the type checker intently look at the code instead of me. It’s worked: I’ve gotten rid of some old bugs in rarely used parts of code and hope new bugs of such a type won’t bother me in the future. (Aside: I hope, this post will persuade you not only to consider using value classes but also to think about the role of types in code quality).
These identifiers aren’t created very frequently but anyway there is overhead (though small). Scala can help here even better with a feature called value classes. Value classes are special wrapper classes that give an ability to enrich objects with more precise types and additional methods without allocation overhead.
Let’s continue considering the example with entity identifiers. We can write:
def processWombatOld(idWombat: Int) = ???
val idUserOld = 42
// Oops, a user is a wombat :)
processWombatOld(idUserOld)
// Solution:
case class IdUser(id: Int) extends AnyVal
case class IdWombat(id: Int) extends AnyVal
case class IdAirplane(id: Int) extends AnyVal
// etc
def processWombatNew(idWombat: IdWombat) = ???
val idUserNew = IdUser(42)
// Error as expected
processWombatNew(idUserNew)
The compiler prevents bad things from happening. However, if you decompile the code with javap
, you won’t see any bytecode difference between processWombatOld
and processWombatNew
– idWombat
will be represented as good old plain int
. All checks are done in compile time.
The secret is in extending AnyVal
class. If we look at Scala’s class hierarchy (picture below; from here), we’ll see that primitive types such as Int
or Double
inherits AnyVal
.
Since Scala 2.10, a user can create its own classes inherited from AnyVal
and the compiler will generate code that won’t be using (instantiating) them directly, but some requirements must be met:
- Must have only the primary constructor with exactly one
val
-parameter whose type is not a value class already. - No
val
s orvar
s members are allowed (by obvious reasons), but withdef
s the compiler can do a trick, so you can add them. - Must have no nested classes, traits, objects.
- Can’t be extended.
- Must be a top-level class or a member of a statically accessible object.
- Can’t define
equals
orhashCode
methods.
Another possible use case of value classes is implicit conversions with the purpose of type enrichments. Look at RichInt
implementation in the Scala standard library:
final class RichInt(val self: Int) extends AnyVal with ScalaNumberProxy[Int] with RangedProxy[Int] {
...
override def abs: Int = math.abs(self)
override def max(that: Int): Int = math.max(self, that)
override def min(that: Int): Int = math.min(self, that)
override def signum: Int = math.signum(self)
...
def to(end: Int): Range.Inclusive = Range.inclusive(self, end)
...
There is also implicit conversion from Int
to RichInt
in Predef
(as well as for other primitives). As you can see, it let us write 1 to 20
that’s equals to new scala.runtime.RichInt(1).to(2)
with nearly zero runtime overhead. It’s cool.
Sometimes value classes are actually instantiated, in particularly:
- In case of runtime type tests such as pattern matching.
- When treated as another type.
- Demonstrative example: in
println(myValue)
println
has a parameter of typeAny
, somyValue
of a value class will be actually allocated and then converted toAny
. Doprintln(myValue.toString)
instead. - In case of assignment to an array.
Also, there is a bug-prone corner case. Obviously, there isn’t any information about value classes at runtime, they’re represented by classes they wrap. So, the reflection mechanism isn’t able to see your value classes in functions’ parameter lists, for example. I’ve discovered a bug in Akka connected with this.
To sum up, in my opinion, value classes is a nice feature of Scala that could help you to make a type system in your code a little more powerful; also it’s used by the runtime to reduce the overhead of enriched types. But when using them, you should be careful with the reflection.
You could find more information about value classes in the official documentation or in more formal SIP document.