Scala 3: Opaque Type Aliases and Open Classes

Dean Wampler
Scala 3
Published in
4 min readNov 30, 2020

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.

January 23, 2024: Thanks to a reader who pointed out a change in the warning behavior as of Scala 3.4. See the paragraph just before Final Thoughts below.

Christmas Lights on Erie Street. © 2020, Dean Wampler. All Rights Reserved.

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:

The 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 toDouble, +, and * 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 Logarithm.

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 equals and 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.

Open Classes

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. Update: As of Scala 3.4, the feature warning for ad-hoc extensions is produced only under -source future flag, by default. See these docs.

Final Thoughts

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!

--

--

Dean Wampler
Scala 3

The person who is wrong on the Internet. ML/AI and FP enthusiast. Lurks at the AI Alliance and IBM Research. Speaker, author, pretend photographer.