In this tutorial, we’ll take a look at how to use the open keyword to signal that a class is open for extension in Scala 3. Then, we’ll focus on its intended use for concrete classes and its motivation.
We’ll first see how the open keyword behaves, then we’ll dive into the motivations for it, and finally, we’ll compare it with the sealed keyword, which is also available in Scala 2.
2. The open Keyword
The open modifier on a class signals that the class is open for extension. Abstract classes and traits are, by default, considered open in Scala 3. Hence, this modifier only makes sense in concrete classes.
open is a “soft” modifier, meaning that it is treated as a modifier only if it is in a precise position, not otherwise. This means that we can define variables or values named open without worrying about the compiler complaining.
open introduces a new possibility for programmers. In Scala 2, we could extend any concrete class unless it was marked as final. Scala 3’s approach is safer, as programmers are now forced to think whether it’s safe for a (concrete) class to be extended and, if it is, they have to state that explicitly.
Let’s see how to use open in practice:
open class Album:
val tracks: List[String] = ???
class DeluxeEdition extends Album:
override val tracks: List[String] = ???
val premiumCode: String = ???
In the example above, Album is a concrete class that is safe to extend. Then, DeluxeEdition inherits from it, modifying the list of tracks and adding a new field, premiumCode. Album has the open modified in its declaration, and that’s why DeluxeEdition can extend it. Also, Album and DeluxeEdition can be defined in two different files.
It’s good practice to document the ways other programmers can extend an open class. For example, this includes the methods that subclasses may override.
2.1. Non-open Classes
Classes that are not marked with open can still be extended but only if at least one of the following conditions is true:
- The two classes (the sub-class and the superclass) are in the same file. This is similar to the behavior of sealed.
- The extending class enables the feature adhocExtensions. There are two ways of doing so. First, the source file can import it (scala.language.adhocExtensions). Secondly, we can enable it as a compiler option: -language:adhocExtensions.
In our example above, if Album were not open, then we could extend it either by defining DeluxeEdition in the same file or by importing the adhocExtensions flag. If we didn’t do any of that and yet had DeluxeEdition inherit from Album, the compiler would show us a warning when compiling the extending class (DeluxeEdition). Still, this is just a warning and won’t cause compilation to fail (unless we configure the compiler differently):
-- Feature Warning: DeluxeEdition.scala:3:30 ----
|class DeluxeEdition extends Album
|Unless class Album is declared 'open', its extension
| in a separate file should be enabled
|by adding the import clause 'import scala.language.adhocExtensions'
|or by setting the compiler option -language:adhocExtensions.
Obviously, an open class cannot be final or sealed. For example, marking the class Album above as both open and final will produce an error at compile-time: illegal combination of modifiers: `final` and `open` for: class Album.
The documentation advises against using adhocExtension, even though there might be some cases where it’s useful.
For example, let’s consider test doubles. Suppose we have a concrete class that uses some external dependencies (database connections, for example). In this case, when unit-testing that class, we want to cut off the database. So, instead of mocking the class, we might want to subclass it, modifying some methods to run a stubbed implementation.
A well-established alternative to test doubles is dependency injection, but that’s not always possible, for example, because we’re working with a concrete class we didn’t write and/or we cannot modify.
Another legitimate use of adhocExtension is for temporary bug fixes. In this case, assume we’re using a library, and we ran into a bug in one of the classes. We can report it and wait for a fix, but we don’t how long that’s going to take. In the meantime, we can just import adhocExtension, subclass the buggy class, put in a temporary patch, and keep working on our code.
For those reasons, Scala 3 allows adhocExtension but only if we import the feature flag explicitly.
2.3. Comparison with sealed
Classes that are neither open nor abstract are similar to sealed classes. As a matter of fact, there is no difference if we define a sub-class in the same file. However, sealed does not allow for extending classes to be defined in different files. open, on the other hand, allows that or shows a warning, depending on the import of adhocExtensions.
2.4. Cross-Compilation With Scala 2
open is a new modifier in Scala 3, and it involves breaking changes. Hence, its introduction will be gradual. As a matter of fact, to allow cross-compilation between Scala 2.13 and Scala 3, the compiler will show the warning for the missing import of adhocExtensions only if we set another compiler flag: -source future. From Scala 3 on, the compiler will show the warning by default.
When we are writing a class and thinking of its extensibility, we have three possible choices:
- Allowing subclassing: In this case, we should design the class for subclassing and document the extension contract carefully. Abstract classes and traits always fall into this category. A use case for extensible concrete classes, on the other hand, might be a framework with some sort of default behavior that can be modified by the users of the framework itself. In Scala 3, all such classes are open.
- Forbidding subclassing: In this case, we don’t want anyone to extend our class to make sure no one modifies its behavior. This might be for security reasons, such as implementing some methods that deal with sensitive information we do not want to expose. In Scala 3, they are final.
- Possibly allowing subclassing: The class was not originally meant to be extended, but there is no real reason against it. For example, we might want to implement a test double as described above. If so, we can still subclass it, but at our own risk. Future versions of the class might introduce breaking changes that we’ll have to deal with. In Scala 3, we achieve such a result by importing adhocExtension and extending the class.
In this article, we saw how to use the open keyword to signal that a class is open for extension in Scala 3. We discussed its motivations, saw how to use it in practice, and compared it with sealed.
As usual, you can find the code over on GitHub.