1. Overview

In Kotlin, enums are powerful constructs that allow us to define a set of named constants representing distinct values. However, when working with multiple enum classes, creating a universal function that provides the functionalities of any enum class can be challenging.

In this tutorial, we’ll explore various approaches to creating a versatile Kotlin function that can be applied to any enum class.

2. Understanding the Challenge

As usual, let’s understand the challenge through an example.

Let’s say we’d like to have a function called joinTheirNames(), which is available for all enum classes to join the given enum’s instance names by commas.

For example, let’s suppose we call this function with the following Level enum:

enum class Level {
    A, B, C, D, E
}

joinTheirNames() returns a string:

"A, B, C, D, E"

Similarly, let’s say we invoke this function on a different enum whose constructor accepts arguments, such as WorkingDay:

enum class WorkingDay(val n: Int) {
    MON(1), TUE(2), WED(3), THU(4), FRI(5)
}

We’ll get the following string as a result:

"MON, TUE, WED, THU, FRI".

The challenge lies in the fact that each enum class in Kotlin is a separate type with its own set of constants. Further, Kotlin doesn’t support enum inheritance, meaning each enum class is independent.

Consequently, writing a function that works for all enum classes requires a thoughtful approach considering their differences.

In this tutorial, we’ll take the joinTheirNames() requirement as an example to address different ways to create a function for all enum classes.

3. Creating an Extension Function on the Array Class

In Kotlin, for any enum type, we can get its instances in an array by calling the values() function. So, we can create an extension function on the Array class to achieve our goal:

fun <E : Enum<E>> Array<E>.joinTheirNames(): String {
    return joinToString { it.name }
}

Of course, we only want to extend array classes with enum instances as elements instead of polluting all arrays. Therefore, as the code above shows, we introduced the type parameter <E : Enum<E>>to make the joinTheirNames() function only available for enum arrays (Array<E>).

The function’s implementation is pretty straightforward. We employed the joinToString() function to concatenate the names of enum constants.

With the extension function, we can call AnyEnumType.values().joinTheirNames() to get the expected string result:

assertEquals("A, B, C, D, E", Level.values().joinTheirNames() )
assertEquals("MON, TUE, WED, THU, FRI", WorkingDay.values().joinTheirNames())

4. Creating an Extension Function on the EnumEntries Class

Since version 1.9.0, Kotlin has introduced the entries property to all enum types as a stable feature. Also, the official document recommended replacing AnEnum.values() function calls with AnEnum.entries.

Unlike the values() function returning an array, the entries property is with the type EnumEntries. Therefore, if we’re working with Kotlin 1.9.0 or later, we can change our extension function from extending Array to extending the EnumEntries class:

fun EnumEntries<*>.joinTheirNames(): String {
    return joinToString { it.name }
}

Then, the extension function can be called with an enum’s entries property:

assertEquals("A, B, C, D, E", Level.entries.joinTheirNames() )
assertEquals("MON, TUE, WED, THU, FRI", WorkingDay.entries.joinTheirNames())

We’ve discussed two solutions to create a function on all enums. However, no matter whether it’s extending Array or EnumEntries, joinToString() is invoked by an enum’s instances instead of from enum classes directly.

So next, let’s see if we can have a more straightforward solution.

5. Can We Make the joinToString() Function at the Class Level?

It would be ideal to call functions like Level.joinTheirNames() and WorkingDay.joinTheirNames(). It’s straightforward and easy to use.

We know that the Enum class is the common supertype of all enum classes in Kotlin. Also, we have seen the power of Kotlin extension functions. Extension functions allow us to add functionalities to a type without really modifying the type’s code.

Therefore, an idea may come up: creating an extension function to extend the Enum class.

However, after making some attempts, we’ll realize that we cannot achieve something like Level.joinTheirNames() by creating an extension function on Enum.

Next, let’s find out why.

5.1. Why Can’t We Make Level.joinTheirNames() Work?

First, it’s worth noting that Kotlin’s extension functions operate at the instance level. For example, if we create an extension function singleCharString() on String:

fun String.singleCharString() = this.length == 1

Then, the singleCharString() function is available for all String instances. For instance, we can call it in this way:

"x y z".singleCharString() // false
"x".singleCharString() // true

However, we cannot call the extension function from the String class like String.singleCharString().

Now, if we look at our “extension functions on Enum” idea again, we aimed to have Level.joinTheirNames() or WorkingDay.joinTheirNames(). But Level and WorkingDay here are two enum classes instead of instances. So, we cannot achieve our goal using extension functions.

Apart from that, let’s say we could achieve WorkingDay.joinTheirNames(). Then, the joinTheirNames() function will be a static function of the WorkingDay class. In Kotlin and Java, static methods aren’t inherited from a superclass to a subclass.

Therefore, even if we could create a static extension function on the Enum class,  the function will be only available for the Enum class but not for its subclasses. In other words, if we could add the static extension joinTheirNames() on the Enum class, we’d have Enum.joinTheirNames() instead of WorkingDay.joinTheirNames().

We may have noticed if we can somehow pass the concrete enum type to Enum.joinTheirNames(), it still solves our problem, although it doesn’t look as straightforward as WorkingDay.joinTheirNames().

Then, let’s figure out if we can create static extension functions in Kotlin.

5.2. Can We Create Static Extension Functions?

Currently, Kotlin doesn’t support static extension functions. But this feature is on Kotlin’s official KEEP proposal.

Does this mean we cannot create a static Enum extension? Before answering this question, let’s look at how the static function works in Kotlin.

Kotlin’s static functions are based on Companion objects. We cannot directly create static extensions, but if the target class has a companion object, we can create extension functions for the companion object. Then, we can call this extension function as we call a static function.

Let’s have a look at the Enum class’s implementation:

public abstract class Enum<E : Enum<E>>(name: String, ordinal: Int): Comparable<E> {
    companion object {}
    ...
}

Fortunately, the Enum class has an anonymous companion object. So next, let’s extend this companion object by adding the joinTheirNames() function:

inline fun <reified E : Enum<E>>Enum.Companion.joinTheirNames(): String {
    return enumValues<E>().joinToString { it.name }
}

As the function shows, we introduced a type parameter to know which concrete enum type we’re working with. Kotlin offers the reified inline enumValues() function to obtain all instances of Enum<T>:

inline fun <reified T : Enum<T>> enumValues(): Array<T>

We used the enumValues() function in our joinTheirNames() function. Therefore, we need to make our type parameter reified, too.

Now, it’s time to verify whether this approach works or not. Let’s test it with our two enum examples:

assertEquals("A, B, C, D, E", Enum.joinTheirNames<Level>())
assertEquals("MON, TUE, WED, THU, FRI", Enum.joinTheirNames<WorkingDay>())

The test passes if we give it a run. So, this approach works.

However, let’s quickly revisit the function call: Enum.joinTheirNames<Level>(). If we’ve already restricted E that must be an enum type, the Enum class prefix is redundant.

Next, let’s see if we can simplify the function.

6. Creating a Generic Function

As we discussed earlier, since we’ve defined that Enum.joinTheirNames() can only work with enum types, the Enum prefix isn’t necessary. So, let’s remove “Enum.Companion” from the function signature:

inline fun <reified E : Enum<E>> joinEnumNames(): String {
    return enumValues<E>().joinToString { it.name }
}

Now, joinEnumNames() becomes a regular function.

Finally, if we test it using our enum examples, it works as expected:

assertEquals("A, B, C, D, E", joinEnumNames<Level>() )
assertEquals("MON, TUE, WED, THU, FRI", joinEnumNames<WorkingDay>())

7. Conclusion

In this article, we explored different approaches to creating a function that is available for all enum classes through examples. We’ve used various Kotlin features in our implementations, such as extension functions, generics, reified type parameters, inline functions, etc.

As always, the complete source code for the examples is available over on GitHub.

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments