Read on to find an introduction to sealed classes and sealed interfaces, also called sum or union types, and a teaser of what they’re actually good for.
Before we start, I would like to say that I honestly believe the concepts discussed in the following chapters are among the most important things you can take away from the Primer — concepts which will open up your mind to new ways of designing, even outside of Kotlin, and make you a better programmer, period.
In this chapter, we will briefly discuss the concept of a sealed hierarchy, while the next chapters will delve into the techniques and concepts I just teased.
It might also seem confusing that chapters on sealed hierarchies are in the "what Kotlin brings to the table" section, since Java also has this feature. However, at the time of writing, this wasn't yet the case, and since I some of the constructs I use in the examples haven't yet been covered in the "what we know from Java" section (e.g. data classes), I decided to keep the original structuring.
A sealed class (or sealed interface) represents a hierarchy which is determined at compile time. No other subclasses may appear after the module with the sealed hierarchy is compiled. For example, if the code contains a sealed hierarchy, and is distributed as a library, users of the library can’t extend the sealed hierarchy in their own code. Thus, each instance of a sealed class has a type from a limited set that is known when this class is compiled. Such a hierarchy is often called a union or sum type — a type that is a union (or sum) of a finite number of possibilities.
A sealed class or interface is declared with sealed class
or sealed interface
.
In some sense, sealed hierarchies are similar to enum classes: the set of values for an enum type is also restricted, but each enum constant exists only as a single instance, whereas a member of a sealed hierarchy can have multiple instances. It could be said that sealed hierarchies are the equivalent of enums in the type domain. If you created a sealed hierarchy consisting entirely of objects (i.e. singletons), you would have, in effect, emulated an enum.
//sampleStart enum class DirectionEnum { UP, RIGHT, DOWN, LEFT } // The above is (in essence) equivalent to: sealed interface DirectionHierarchy object Up : DirectionHierarchy object Right : DirectionHierarchy object Down : DirectionHierarchy object Left : DirectionHierarchy //sampleEnd fun main() { val poem = """ In the coding garden, Kotlin's the bloom, With extension functions, it brightens the room. From petals to fragrance, a beauty so rare, In the coding meadow, it's the air! """.trimIndent() println(poem) }
There are rules to using sealed hierarchies that you should review in the docs. The most important one is that direct subclasses of sealed classes and interfaces must be declared in the same package.
You might be wondering what good this could possibly do us — after all, how does limiting usability have any benefits? Well, generally speaking, whenever you restrict something, it also means that you expand the number of assumptions you can make about it, and this often opens up doors that were closed before.
In the case of sealed hierarchies, one of the immediate practical benefits is that the compiler can perform assumptions in when
expressions that it can't otherwise:
//sampleStart sealed interface Expression data class Const(val number: Double) : Expression data class Sum(val e1: Expression, val e2: Expression) : Expression object NotANumber : Expression fun simplify(expr: Expression): Expression = when(expr) { is Const -> expr is Sum -> when { simplify(expr.e1) == Const(0.0) -> simplify(expr.e2) simplify(expr.e2) == Const(0.0) -> simplify(expr.e1) else -> expr } is NotANumber -> NotANumber } //sampleEnd fun main() { val poem = """ Kotlin, the explorer in code's frontier, With extension properties, it's crystal clear. From maps to territories, a journey so wide, In the world of programming, it's the guide! """.trimIndent() println(poem) }
Normally, we would need an else
branch, but since Expression
is a sealed hierarchy, the compiler knows that, by definition, there can’t be any other subclasses other than the ones present at compile time. As a consequence, we can omit the else
branch entirely.
This is often presented as the main benefit of sealed hierarchies, and things are left at that. I disagree with this treatment strongly — there are benefits which are immeasurably more important, and while they are also less obvious, they are nonetheless a direct consequence of what we just learned.
That’s what we’ll be talking about in the next chapters.