DEV Community

loading...
Cover image for Matt's Tidbits #62 - When you're feeling exhausted...

Matt's Tidbits #62 - When you're feeling exhausted...

Matthew Groves
Software engineer with 15+ years of professional experience in C++, C#, Java, and Kotlin.
Originally published at Medium ・4 min read

Last time I wrote about another issue regarding version 3.6.1 of the Android Gradle Plugin. This week, I have some information to share about when statements in Kotlin.

The when statement in Kotlin is analogous to the switch statement in other C-like languages. However, it’s been given a few additional capabilities.

With a switch statement, one can only use it to check against primitive types (such as integers), Strings, or enums. In Kotlin, the when statement can also be used with sealed classes.

One of the benefits of a sealed class is that it has additional features on top of an enum, but with the same property that all options are defined at compile-time.

On top of this, Kotlin supports a feature known as exhaustive when expressions, where if your when statement does not list all of the possible options (or use an else branch), you will get a compiler error.

From the Kotlin documentation:

If when is used as an expression, the else branch is mandatory, unless the compiler can prove that all possible cases are covered with branch conditions (as, for example, with enum class entries and sealed class subtypes).

This is a really useful feature in that it allows you to force clients to handle new cases that might be added in the future, potentially preventing future bugs from occurring.

However, it’s important to note that not all uses of the when operator are exhaustive. For example, the following when statement will not have its completeness enforced by the compiler:

val x = 5
when(x) {
  1 -> doSomething()
  5 -> doSomethingElse()
}
Enter fullscreen mode Exit fullscreen mode

The reason for this is that the result of the when is not used by anyone. However, if you instead do the following, the compiler will enforce the completeness:

val x = 5
val result = when(x) {
  1 -> doSomething()
  5 -> doSomethingElse()
}
//☝️ Won't compile, because there may be other possible integer values you haven't accounted for!
Enter fullscreen mode Exit fullscreen mode

Personally, I find it a little irritating that when statements aren’t exhaustive in all cases. And, I think it can be a little cumbersome to have to assign the output of the when statement to a variable when you don’t need to use it — this would cause a lint warning if you don’t use result, and someone else may later come along and take Android Studio’s suggestion to remove result, which will also remove the exhaustive-ness from this statement!

I did some internet searching, and discussed this with my team, and we came up with a few options to help solve this problem:

Option A

// Definition
val Any?.exhaustive get() = Unit
// Usage
when (foo) { ... }.exhaustive
Enter fullscreen mode Exit fullscreen mode

Pros: really clean syntax, clear, easy to use!
Cons: since exhaustive is declared as an extension function on the Any type, it will be globally suggested as an option to apply to all types by Android Studio, not just when statements.

Option B

// Definition
object Do {
  inline infix fun<reified T> exhaustive(any: T?) = any
}
// Usage
Do exhaustive when(foo) { ... }
Enter fullscreen mode Exit fullscreen mode

Pros: syntax is still pretty clear, doesn’t pollute the global namespace!
Cons: a little more verbose than Option A

Option C

// Definition
object Exhaustive {
  operator fun invoke(any: Any?) = any
}
// Usage
Exhaustive(
  when(foo) { ... }
)
Enter fullscreen mode Exit fullscreen mode

Pros: shorter syntax than Option B, and you get to use the fun operator invoke trick!
Cons: the extra required parenthesis make this a little clunkier (believe me, I tried everything I could think of to get rid of them)

Option D

val action = when(sealedClass) {
  is sealedClass.A -> ::doSomething
  is sealedClass.B -> ::doSomethingElse
}
action()
Enter fullscreen mode Exit fullscreen mode

Pros: No funky definitions required, and this is exhaustive!
Cons: Does not work if you need to pass parameters to your functions

Option E

val sealedClass = someMethodThatReturnsASealedClass()
handleSealedClassOptions(sealedClass)
fun handleSealedClassOptions(sealedClass: SealedClass) = 
  when(sealedClass) {
    is sealedClass.A -> doSomething(sealedClass)
    is sealedClass.B -> doSomethingElse(sealedClass)
  }
Enter fullscreen mode Exit fullscreen mode

Pros: Very clean syntax, makes surrounding code easier to read
Cons: May have to define a unique method for each call site

Summary

It’s important to note that there’s not necessarily a *best* option here. Each one has its advantages. Personally, I will probably be using Option E in my app, as I like how it will allow me to force exhaustiveness for all uses of a particular type of Sealed Class, and in the places where I’m intending to use this (RxJava subscribe() blocks), will be much easier to read.

Note that from talking to the rest of my team, some people feel that in some cases forcing a when to be exhaustive may be a bad practice, and you should follow the language’s setup and only have when statements be exhaustive in cases where the language supports it (you’re doing something with the resulting value of the when statement). So, it’s really one of those things where it just depends on your unique situation/preferences.

I hope you learned something useful about when statements! If you have other solutions to this problem, or want to share your preferences/opinion on this topic, leave a comment below! And, please follow me on Medium if you’re interested in being notified of future tidbits.

Interested in joining the awesome team here at Intrepid? We’re hiring!

This tidbit was originally delivered on March 27, 2020.

Discussion (0)