Scala 3: Opaque Type Aliases and Open Classes
In this post, I begin a tour of many of the changes to Scala’s type system in Scala 3. Let’s start with two relatively small changes, opaque type aliases and open classes, which have nice properties for better performance and better code quality.
December 4, 2020: Clarified the tradeoffs between value classes and opaque type aliases, following comments by Martin Odersky in a contributors mailing list thread.
Opaque Type Aliases
It’s common to have small classes that represent domain concepts and operations, but are backed by a single
AnyVal, like a
Double. In some cases, you really want the performance of having just a
Double for a value, because it can be pushed on the stack and stored in a register, whereas a normal class involves a heap allocation, fetching from memory, inevitable cache misses, etc. In many performance sensitive applications, like big data scenarios, this is overhead you want to avoid.
For a while, Scala has had value classes to give us the benefit of working with a “rich” type, while using an underlying
AnyVal in byte code. Unfortunately, value classes have several drawbacks. For example, there are many scenarios where a heap allocation has to be used (“boxing”), undermining the performance benefit.
Scala 3 introduces an alternative called opaque type aliases. Here’s an example adapted from the Dotty documentation:
opaque keyword marks the type alias for
Logarithm as special. Construction of instances is provided by the
Logarithm object, analogous to a companion object for classes. You do have to define methods like
apply yourself, unlike for case classes, where the compiler can generate them.
Any methods you want on
Logarithms are implemented as extension methods, like
* here. You can’t just call
Double methods on a
Logarithm. You either have to define an extension method for each
Double method you want or use
toDouble, call the method, then convert back to a
Here it is in action:
Usage looks like any other type, but the compiler uses
Double for “instances”.
(Updated) I think opaque type aliases will be very popular, replacing value classes in many cases. Value classes still have a few advantages. They are real classes, so you can customize the
toString methods for them and you can pattern match on them (reasons that boxing is occasionally necessary). Opaque type aliases don’t provide these benefits. Finally, the JDK will eventually have its own form of value classes (the Valhalla project), in which case a Scala representation will be desirable.
Scala encourages you to design type hierarchies carefully, such as using sealed hierarchies to prevent subtyping except within the same source file. You can also declare types and methods
final, so that subtyping and overriding, respectively, are disallowed.
Otherwise, classes have been open for subtyping, i.e., ad-hoc extensions. Even if you’re rigorous about marking types
final when they shouldn’t be subtyped, there are legitimate situations when you want to relax that restriction.
My favorite example of the latter is for creating test doubles. I frequently subclass a type that should be “effectively final” in the production code, but I’ll override a method to insert stubbed behavior, for example to prevent a database query during a unit test. I find this far more convenient than using mocks (a subject for another time…).
However, if I have marked the type
final, I can’t do this. The best I can do is make the database connection a constructor argument (dependency injection) and pass in a test double there. That’s a very good pattern, too, but sometimes I really want the flexibility of the subtyping hack.
Scala 3 addresses all these issues with a change to when concrete types are considered open or closed. Abstract classes and traits are by definition open, so this only applies to concrete classes.
Now, if a class is meant to be open for subtyping, it has to be declared with the
open keyword or the import statement
import scala.language.adhocExtensions has to be used.
This is better than
final for two reasons. First, it’s now the default that a concrete class is closed unless it’s marked open, enforcing better practice. Second, if you mark a class
final, you can never subclass it, as I said, but with a class that isn’t marked
open, you can circumvent the subtyping restriction by using the import statement, for example in your tests.
This behavior change will break existing code, so it is being introduced gradually. In Scala 3.0, the feature warning is only emitted if you use the compiler flag
-source 3.1. It will be turned on by default in Scala 3.1.
Opaque type aliases and open classes are small additions, but both provide nice software development benefits. For opaque type aliases, we get the performance of
AnyVals with the convenience of rich domain types. With the
open keyword, we get tighter control over ad-hoc extensions by default, with flexibility when we need it.
In my next post, I’ll discuss intersection and union types.
You can start reading the rough draft of Programming Scala, Third Edition on the O’Reilly Learning Platform. The first half or so of the chapters are available. I am refining them still, but any feedback is welcome!