1. Introduction

Simply put, the Kotlin language borrowed a number of concepts from other functional languages to help with writing safer and more readable code. Sealed hierarchies are one of these concepts.

2. What Is a Sealed Class?

Sealed Classes allow us to fix type hierarchies and forbid developers from creating new subclasses.

They are useful when we have a very strict inheritance hierarchy, with a specific set of possible subclasses and no others. As of Kotlin 1.5, sealed classes can have subclasses in all files of the same compilation unit and the same package.

Sealed classes are also implicitly abstract. They should be treated as such throughout the rest of our code, except that nothing else is able to implement them.

Sealed classes can have fields and methods defined in them, including both abstract and implemented functions. This means that we can have a base representation of the class and then adjust it to suit the subclasses.

2.1. Sealed Interfaces

As of Kotlin 1.5, interfaces can also have the sealed modifier, which works on interfaces in the same way it works on classes: all implementations of a sealed interface should be known at compile time.

One of the advantages of sealed interfaces over sealed classes is the ability to inherit from multiple sealed interfaces. This is impossible for sealed classes because of the lack of multiple inheritance in Kotlin.

3. When to Use Sealed Classes?

Sealed classes are designed to be used when there are a very specific set of possible options for a value, and where each of these options is functionally different – just Algebraic Data Types.

Common use cases might include implementing a State Machine or in Monadic Programming, which is becoming increasingly more popular with the advent of functional programming concepts.

Anytime we have multiple options and they only differ in the meaning of the data, we may be better off using Enum Classes instead.

Anytime we have an unknown number of options, we can not use a sealed class because this will stop us from adding options outside of the original source file.

4. Writing Sealed Classes

Let’s start by writing our own sealed class – the good example of such sealed hierarchy is an Optional from Java 8 – which can be either Some or None.

When implementing this, it makes a lot of sense to restrict the possibility of creating new implementations – the two provided implementations are exhaustive and no one should add their own.

As such, we can implement this:

sealed class Optional<out V> {
    // ...
    abstract fun isPresent(): Boolean
}

data class Some<out V>(val value: V) : Optional<V>() {
    // ...
    override fun isPresent(): Boolean = true
}

class None<out V> : Optional<V>() {
    // ...
    override fun isPresent(): Boolean = false
}

It can now be guaranteed that any time we have an instance of Optional<V>, we actually have either a Some<V> or a None<V>.

In Java 8, the actual implementation looks different because of the absence of sealed classes.

We can then make use of this in our computations:

val result: Optional<String> = divide(1, 0)
println(result.isPresent())
if (result is Some) {
    println(result.value)
}

The first line will either return a Some or a None. We then output whether or not we got a result.

5. Use With When

Kotlin has support for using sealed classes in its when constructs. Because there are always an exact set of possible subclasses, the compiler is able to warn us if any branch is not handled, in exactly the same way that it does for enumerations.

This means that in such situations, there is normally no need for a catch-all handler, which in turn means that adding a new subclass is automatically safe – the compiler will immediately warn us if we haven’t handled it, and we will need to fix such errors before continuing.

The above example can be extended to output either the result of an error depending on the type returned:

val message = when (result) {
    is Some -> "Answer: ${result.value}"
    is None -> "No result"
}
println(message)

If either of the two branches were missing, this would not compile and instead result in an error of:

'when' expression must be exhaustive, add necessary 'else' branch

6. Summary

Sealed classes can be an invaluable tool for our API design toolbox. Allowing a well-known, structured class hierarchy that can only ever be one of an expected set of classes can help remove a whole set of potential error conditions from our code, whilst still making things easy to read and maintain.

As always, code snippets can be found over on GitHub.

Comments are closed on this article!