With sealed classes you can use exhaustive when
expressions and omit the else
clause when the expression returns a result:
sealed c
We can create an extension property on type T with a name that helps explain the purpose
val <T> T.exhaustive: T
get() = this
and then use it anywhere like
when (sealedClass) {
is SealedClass.First -> doSomething()
is SealedClass.Second -> doSomethingElse()
}.exhaustive
It is readable, shows exactly what it does an will show an error if all cases are not covered. Read more here
In inspiration by Voddan's answer, you can build a property called safe
you can use:
val Any?.safe get() = Unit
To use:
when (sealedClass) {
is SealedClass.First -> doSomething()
is SealedClass.Second -> doSomethingElse()
}.safe
I think it provides a clearer message than just appending .let{}
or assigning the result to a value.
There is an open issue on the Kotlin tracker which considers to support 'sealed whens'.
A discussion triggered me to look for a more general solution and found one, for Gradle builds. It doesn't require changing the source code! The drawback is that compilation may become noisy.
build.gradle.kts
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
val taskOutput = StringBuilder()
logging.level = LogLevel.INFO
logging.addStandardOutputListener { taskOutput.append(it) }
doLast {
fun CharSequence.hasInfoWithError(): Boolean =
"'when' expression on sealed classes is recommended to be exhaustive" in this
if (taskOutput.hasInfoWithError()) {
throw Exception("kotlinc infos considered as errors found, see compiler output for details.")
}
}
}
build.gradle
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
def taskOutput = new StringBuilder()
logging.level = LogLevel.INFO
logging.addStandardOutputListener(new StandardOutputListener() {
void onOutput(CharSequence text) { taskOutput.append(text) }
})
doLast {
def hasInfoWithError = { CharSequence output ->
output.contains("'when' expression on sealed classes is recommended to be exhaustive")
}
if (hasInfoWithError(taskOutput)) {
throw new Exception("kotlinc infos considered as errors found, see compiler output for details.")
}
}
}
Notes:
hasInfoWithError
to generalize to other i:
s.subprojects { }
or allprojects { }
to apply project-wide.References:
kotlinOptions.allWarningsAsErrors
would solve the issue)The way to enforce exhaustive when
is to make it an expression by using its value:
sealed class SealedClass {
class First : SealedClass()
class Second : SealedClass()
class Third : SealedClass()
}
fun test(sealedClass: SealedClass) {
val x = when (sealedClass) {
is SealedClass.First -> doSomething()
is SealedClass.Second -> doSomethingElse()
} // ERROR here
// or
when (sealedClass) {
is SealedClass.First -> doSomething()
is SealedClass.Second -> doSomethingElse()
}.let {} // ERROR here
}
Consider using the recent library by JakeWharton that allows to just use @Exhaustive
annotation.
sealed class RouletteColor {
object Red : RouletteColor()
object Black : RouletteColor()
object Green : RouletteColor()
}
fun printColor(color: RouletteColor) {
@Exhaustive
when (color) {
RouletteColor.Red -> println("red")
RouletteColor.Black -> println("black")
}
}
Usage:
buildscript {
dependencies {
classpath 'app.cash.exhaustive:exhaustive-gradle:0.1.1'
}
repositories {
mavenCentral()
}
}
apply plugin: 'org.jetbrains.kotlin.jvm' // or .android or .multiplatform or .js
apply plugin: 'app.cash.exhaustive'
Lib: https://github.com/cashapp/exhaustive
Our approach avoids to have the function everywhere when auto-completing. With this solution you also have the when return type in compile time so you can continue using functions of the when return type.
Do exhaustive when (sealedClass) {
is SealedClass.First -> doSomething()
is SealedClass.Second -> doSomethingElse()
}
You can define this object like so:
object Do {
inline infix fun<reified T> exhaustive(any: T?) = any
}