Article
sealed-enum: Replacing Enums in Kotlin
November 2, 2020
This is the origin story for https://github.com/livefront/sealed-enum , a Kotlin annotation processor for generating enum-like objects from sealed classes of objects. For installation and detailed usage, see the project’s documentation, but to learn more about why it exists and how you can replace normal enums, read on!
Sealed classes are one of Kotlin’s powerful tools to express hierarchies and precisely enumerate the form that data takes. With the when
keyword, working with sealed classes can be simple and safe, with no need for an annoying else
branch. When initially presented, especially for someone coming from a Java background, they appear to be even more powerful enums; indeed, the official Kotlin documentation ¹ states that sealed classes
are in a sense, an extension of enum classes.
Ever since I learned about sealed classes, I’ve used them constantly. Nesting sealed classes allows expressing complex state in an incredibly powerful way, and serializing and deserializing them is effortless with kotlinx.serialization . Generic sealed classes allow convenient handling for common wrappers, like State<T>
orResult<T>
.
Yet with everything that sealed classes can do, I still found myself writing the following code:
For how I was using ScrollingBehavior
, I could have just as easily made it a sealed class:
With when
, the code to consume the enum class and the sealed class would be pretty much identical. So why do we need still need enums at all? A sealed class of only objects is strictly a more powerful enum: they are both a static, finite enumeration of something. Just like an enum, the sealed class can have constructor arguments or abstract values, and implement interfaces. However, sealed classes are much more flexible than enum classes: they can extend other classes, have generic type arguments, or be part of a nested sealed class hierarchy.
In the scenario above, I didn’t need to deal with the full list of ScrollingBehavior
s. If I had, the enum class would have been the more convenient approach due to the existence of ScrollingBehavior.values()
. Accomplishing the same for sealed classes is not as simple. The only built-in way is with KClass.sealedSubclasses
which works, but requires Kotlin reflection, which can be unacceptably slow at runtime. The alternative is maintaining the list of objects manually ² , which also works, but is error-prone boilerplate — but boilerplate can be automated away!
Furthermore, there’s an opportunity here for improved generic handling, since dealing with enums generically is painful. enumValuesOf<T>()
works if it is possible to access the enum type in a reified way, but if it isn’t, the main option is to pass through the Class
of the enum for Class.getEnumConstants
.
sealed-enum
was born to solve both of these problems, and in-effect make defining actual enum classes obsolete in favor of sealed classes of objects, which I call “sealed enums”. Taking inspiration from kotlinx.serialization’s KSerializer
objects, annotating the companion object of a properly structured sealed class T
with @GenSealedEnum
will result in a generated implementation of SealedEnum<T>
. That implementation is an object, which provides the values
list and ordinalOf
method, implements Comparator
based on those ordinals, and also includes nameOf
and valueOf
methods:
Together, these methods match all of the features of normal enum classes.
For an example of how this works, let’s first add sealed-enum
as a Gradle dependency to our project:
The sealed-enum
library consists of two parts: a small runtime library, which defines the annotations and the interfaces, and an annotation processor for kapt
.
Now that we have the library installed, let’s annotate a newly created companion object for the ScrollingBehavior
sealed class above:
The annotation processor then generates a ScrollingBehaviorSealedEnum
object, which implements SealedEnum<ScrollingBehavior>
, as well as extension properties and functions for ease-of-use:
Now, by retrieving ScrollingBehavior.values
, or by passing the SealedEnum
object around directly, we have all of the features we want from an enum and get to keep all of the extra power that sealed classes offer.
The runtime library and annotation processor also have built-in tools to specify behavior and provide interoperability.
@GenSealedEnum
can be configured to specify a traversal order, to switch between in-order, pre-order, post-order and level-order traversal for sealed class hierarchies.
For interoperability, there is built-in functionality for both directions: First, there are methods provided in the runtime library to create a SealedEnum
object given a normal enum class. Second, @GenSealedEnum
can be configured to generate an isomorphic enum class that implements all of the interfaces that the sealed enum does via interface delegation. If you need to specify an enum value in an annotation, or provide a true enum class to some other interface, you can easily map between the two representations as needed.
Enum classes are great, but sealed classes with objects can be better. By generating the remaining boilerplate to fill in the gaps, you too can upgrade your enums to sealed enums, and never write a normal enum class ever again.
Alex works at Livefront , where we write code to test code-writing code.¹: ^ https://kotlinlang.org/docs/reference/sealed-classes.html
²: ^ This approach matches the result of this discussion: https://discuss.kotlinlang.org/t/list-of-sealed-class-objects/10087
-
Alex Vanyo
Software Engineer