Sealed classes and traits in Scala

Alonso Del Arte
Sep 16 · 5 min read
Photo by Amy Asher on Unsplash

Yesterday, September 14, was the official release date of Java 17, the first Long Term Support (LTS) release in what feels like ages (the previous LTS release was Java 11, which came out three years ago).

One of the new features of Java 17 is the addition of sealed classes and interfaces. Which feels quite new to those who only program in Java, but hardly a surprise for those who also program in Scala.

I wrote last week that the future of Java is to absorb Scala features. Not every feature, of course, just the ones that are considered beneficial and which can be brought into Java without breaking backwards compatibility.

Sealed classes and sealed traits in Scala have been deemed worthy for Java to have. Adding the sealed modifier to precede “class” or “interface” should not break existing programs, since they wouldn’t have that word in that context if they were valid programs for earlier Java versions.

Just because Scala programmers were not surprised by this recent addition to Java doesn’t necessarily mean that they know exactly how it works or that they have used the feature in their own Scala programs.

In this article, I’m going to give an overview of the basics of sealed classes and traits in Scala. I will also give a couple of concrete examples that hopefully give you some ideas about how to use sealed classes and traits in your own Scala programs.

In both Java and Scala, a class can be marked final (of course it’s pointless for a Java interface to be final). If it’s not final, a non-private class can be extended by any other class in the same package, or by any class in the module or the “world” if it’s public.

In some cases, though, you might want to allow a class to be extended but only under certain very specific conditions.

The access modifiers, even with the ability in Scala to specify scope of access more precisely than in Java, might still not be as fine-grained as you want it to be for pattern matching in a Match-Case statement.

Suppose for example that you want to match a java.net.URLConnection object according to its subclass at the next lower level of inheritance (without regard for the runtime class).

That object could be an HttpURLConnection or a JarURLConnection. Those might be the only “major” types in the Java Development Kit, but they’re not guaranteed to be the only ones on a particular system configuration.

So you would have to provide a Default Case for an unexpected major type, like maybe FtpURLConnection, or else tolerate a non-exhaustive match warning, and the potential of a MatchError at runtime.

The same would also be true of class hierarchies that you create with Scala. But in Scala, you’ve always had a third option: seal the class.

And now, with Java 17 (or earlier with Java 15 if you enabled the preview features), you also have that option in Java.

Sealing the class means that it may only be extended by classes (or traits, but not interfaces) that you specify. Attempting to extend a sealed class by a class that was not so specified results in a compilation error.

In Scala, you specify that by placing the subclasses or subtraits in the same source file, as you’ll see in the example in a moment.

In Java it looks like you have to write a Permits clause (e.g., “sealed class Shape permits Circle, Triangle, Square,” etc.), and I’m guessing those must be in separate source files.

I don’t think the Shape class is a good use case for sealed classes. Maybe chess pieces are a better use case, since the number of pieces is much more limited than the number of 2-dimensional shapes.

As you know, chess is a game for two players. Each player gets sixteen pieces: one king, one queen, two each of bishops, knights and rooks, and eight pawns. The different pieces could be represented by instances of different classes with a common superclass.

If we seal that common superclass, then the compiler knows that these are the only subclasses that exist of that superclass.

With the sealed Piece class, the compiler now knows that a Piece instance can only be a King, a Queen, a Bishop, a Knight, a Rook or a Pawn. If we want a few more pieces, like maybe an archbishop and a chancellor, we just need to add them to the Piece source file.

Notice that Pawn is abstract. But maybe all of these need to be abstract. Please feel free to play around with this example.

You might be getting the feeling that sealed classes in Scala are like enumerated types (or “enums”) in Java. And sometimes they’re used for that purpose. We could have, for example,

sealed class DayOfTheWeekobject SUNDAYobject MONDAYobject TUESDAY// etc.

Remember that, as far as the Java Virtual Machine (JVM) is concerned, there is no such thing as an enumerated type or “enum,” only final classes E that extend Enum<E>.

That gave me the idea to try to write a sealed Scala class that extends java.lang.Enum[E]. It was a rabbit hole of problems. Sealed classes and traits just weren’t intended as an answer to Java’s enumerated types.

From what I can tell, the differences between sealed abstract classes and sealed traits are the same as the differences between non-sealed abstract classes and non-sealed traits.

That means, for one thing, that a class can only extend one subclass of a sealed class, but a class can implement multiple subtraits of a sealed trait.

Consider this example:

Then we could have something like:

class Knight extends Piece with CanJumpOver with SomeOtherTrait {  override val notation: String = "N"  override val minimumMoveDistance: Int = 3  override val maximumMoveDistance: Int = 3}

The way I have set this up, Knight can extend Piece directly but can’t implement Movable directly. Maybe that’s the right design for this particular use case.

I don’t know if there’s a way to circumvent the seals at runtime, but I do know that the local Scala REPL will not allow it.

scala> class Archbishop extends chess.pieces.Piece with chess.pieces.CanJumpOver
^
error: illegal inheritance from sealed class Piece
scala> class Chancellor extends chess.pieces.Movable
^
error: illegal inheritance from sealed trait Movable

I have not yet looked at how the Scala compiler compiles sealed classes and traits, nor how those appear to the JVM. It’s probably going beyond the basics.

In a nutshell, the basic idea of sealed classes and traits in Scala is that they enable you to limit the inheritance hierarchy at one level without limiting it at any of the levels below the immediate subclasses or subtraits.

There you have it, the basics of sealed classes and traits in Scala. I hope that this introduction gives you some ideas as to how to use them in your own Scala projects.

CodeX

Everything connected with Tech & Code. Follow to join our 500K+ monthly readers