nhaarman
nhaarman

Reputation: 100408

Force compilation error with sealed classes

With sealed classes you can use exhaustive when expressions and omit the else clause when the expression returns a result:

sealed class SealedClass {
  class First : SealedClass()
  class Second : SealedClass()
}

fun test(sealedClass: SealedClass) : String =
    when (sealedClass) {
      is SealedClass.First -> "First"
      is SealedClass.Second -> "Second"
    }

Now if I were to add a Third to SealedClass, the compiler will complain that the when expression in test() is not exhaustive, and I need to add a clause for Third or else.

I am wondering however if this check can also be enforced when test() does not return anything:

fun test(sealedClass: SealedClass) {
    when (sealedClass) {
      is SealedClass.First -> doSomething()
      is SealedClass.Second -> doSomethingElse()
    }
}

This snippet does not break if Third is added. I can add a return statement before when, but this could easily be forgotten and may break if the return type of one of the clauses is not Unit.

How can I make sure I don't forget to add a branch to my when clauses?

Upvotes: 49

Views: 6413

Answers (7)

Pavel Kuznetsov
Pavel Kuznetsov

Reputation: 344

You can override unary operator to make it less verbose:

operator fun Any.unaryMinus() = Unit

And use it like this:

-when (sealedClass) {
    is SealedClass.First -> doSomething()
    is SealedClass.Second -> doSomethingElse()
}

Based on this answer

Upvotes: 0

fkdplc
fkdplc

Reputation: 31

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

Upvotes: 1

nhaarman
nhaarman

Reputation: 100408

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'.

Upvotes: 27

TWiStErRob
TWiStErRob

Reputation: 46480

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:

  • Change implementation of hasInfoWithError to generalize to other i:s.
  • Put this code in subprojects { } or allprojects { } to apply project-wide.

References:

Upvotes: 2

Atiq
Atiq

Reputation: 14835

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

Upvotes: 11

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
}

Upvotes: 27

voddan
voddan

Reputation: 33789

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
}

Upvotes: 27

Related Questions