Kotlin when: A switch with Superpowers

Kotlin when: A switch with Superpowers

There are two kinds of innovation: new perspectives that change how we look at things and pragmatic improvements that change how we do things. Kotlin is full of these pragmatic improvements, getting its user a language that just feels good to use. One of the most useful improvements, especially if you come from Java, is the when construct.

A traditional switch is basically just a statement that can substitute a series of if/else that make simple checks. However, it cannot replace all sorts of if/else sequences but just those which compare a value with some constant. So, you can only use a switch to perform an action when one specific variable has a certain precise value, like in the following example:

switch(number) {
    case 0:
        System.out.println("Invalid number");
        break;
    case 1:
        System.out.println("Number too low");
        break;
    case 2:
        System.out.println("Number too low");
        break;
    case 3:
        System.out.println("Number correct");
        break;
    case 4:
        System.out.println("Number too high, but acceptable");
        break;
    default:
        System.out.println("Number too high");
        break;
}

This is quite limited and while is better than having nothing1, it is useful only in a few circumstances. Instead, the Kotlin’s when can offer much more than that:

  • it can be used as an expression or a statement (i.e., it can return a value or not)
  • it has a better and safer design
  • it can have arbitrary condition expressions
  • it can be used without an argument

Let’s see an example of all of these features.

A Better Design

First of all, when has a better design. It is more concise and powerful than a traditional switch.

Let’s see the equivalent of the previous switch statement.

when(number) {
    0 -> println("Invalid number")
    1, 2 -> println("Number too low")
    3 -> println("Number correct")
    4 -> println("Number too high, but acceptable")
    else -> println("Number too high")
}

Compared to your typical switch, when is more concise:

  • no complex case/break groups, only the condition followed by ->
  • it can group two or more equivalent choices, separating them with a comma

Instead of having a default branch, when has an else branch. The else branch is required if when is used as an expression. So, if when returns a value, there must be an else branch.

var result = when(number) {
    0 -> "Invalid number"
    1, 2 -> "Number too low"
    3 -> "Number correct"
    4 -> "Number too high, but acceptable"
    else -> "Number too high"
}
// it prints when returned "Number too low"
println("when returned \"$result\"")

This is due to the safe approach of Kotlin. This way there are fewer bugs because it can guarantee that it always assigns a proper value.

The only exception to this rule is if the compiler can guarantee that when always returns a value. So, if the normal branches cover all possible values then there is no need for an else branch.

val check = true

val result = when(check) {
    true -> println("it's true")
    false -> println("it's false")
}

Given that check has a Boolean type it can only have two possible values, so the two branches cover all cases: this when expression is guaranteed to assign a valid value to result.

Arbitrary Condition Branches

The when construct can also have arbitrary conditions, not just simple constants.

For instance, it can have a range as a condition.

var result = when(number) {
    0 -> "Invalid number"
    1, 2 -> "Number too low"
    3 -> "Number correct"
    in 4..10 -> "Number too high, but acceptable"
    !in 100..Int.MAX_VALUE -> "Number too high, but solvable"
    else -> "Number too high"
}

This example also shows something important about the behavior of when. If you think about the 5th branch, the one with the negative range check, you will notice something odd: it actually covers all the previous branches, too. That is to say, if a number is 0, is also not between 100 and the maximum value of Int, and obviously the same can be said about 1 or 6, so the branches overlap.

This is an interesting feature, but it can lead to confusion and bugs if you are not aware of it. The ambiguity is solved simply by the order in which the branches are written. The construct when can have branches that overlap, in case of multiple matches the first branch is chosen. This means that is important to pay attention to the order in which you write the branches: it is not irrelevant, it has meaning and can have consequences.

The range expressions are not the only complex conditions that can be used. The when construct can also use functions, is expressions, etc. as conditions.

fun isValidType(x: Any) = when(x) {
    is String -> print("It's a string")
    specialType(x) -> print("It's an acceptable type")
    else -> false
}

Smart Casts with when

If you use an is expression, you get a smart cast for free: so you can directly use the value without any further checks. Like in the following example.

when (x) {
    is Int -> print(x + 1)
    is String -> print(x.length + 1)
    is IntArray -> print(x.sum())
}

Remember that the usual rules of smart casts apply: you cannot use smart casts with variable properties, because the compiler cannot guarantee that they were not modified somewhere else in the code. You can use them with normal (unmodifiable) variables.

data class ExampleClass(var x: Any)

fun main(args: Array<String>) {
    var x:Any = ""
    
    // variable x is OK
    when (x) {
        is Int -> print(x + 1)
        is String -> print(x.length + 1)
        is IntArray -> print(x.sum())
    }    
    
    val example = ExampleClass("hello")

    // variable property example.x is not OK
    when (example.x) {
        is Int -> print(example.x + 1)
        is String -> print(example.x.length + 1)
        is IntArray -> print(example.sum())
    }
}

This is the error that you see if you use IntelliJ IDEA and attempt to use smart cast with variable properties.

Smart cast error

The Type of a when Condition

In short, when is an expressive and powerful construct, that can be used whenever you need to deal with multiple possibilities.

What you cannot do, is use conditions that return incompatible types. You can use a function that accepts any argument, but it must return a type compatible with the type of the argument of the when construct.

For instance, if the argument is of type Int you can use a function that returns an Int, but not a String or a Boolean.

var result = when(number) {
    0 -> "Invalid number"
    // OK: check returns an Int
    check(number) -> "Valid number"
    // OK: checkString returns an Int, even though it accepts a String argument
    checkString(text) -> "Valid number"
    // ERROR: not valid
    false -> "Invalid condition"
    else -> "Number too high"
}

In this case, the false condition is an example of an invalid condition, that you cannot use with an argument of type Int. On the other hand, you can use functions that accept any kind or number of arguments, as long as they return an Int or a type compatible with it.

Among the types compatible with Int, and any other type, there is Nothing. This is a special type that tells the Kotlin compiler that the execution of the program stops there. You can obviously use it on the right of the condition, inside the code of the branch, but you can also use it as a condition.

var result = when(number) {
    0 -> "Invalid number"
    1 -> "Number correct"
    throw IllegalArgumentException("Invalid number") -> "Unreachable code"
    else -> "Everything is normal" 
}

Of course, if you do that the exception will be thrown if no previous condition is matched. So, in this example, if number is not 0 or 1 the code will throw an IllegalArgumentException.

Using when Without an Argument

The last interesting feature of when is that it can be used without an argument. In such case it acts as a nicer if-else chain: the conditions are Boolean expressions. As always, the first branch that matches is chosen. Given that these are boolean expression, it means that the first condition that results True is chosen.

when {
    number > 5 -> print("number is higher than five")
    text == "hello" -> print("number is low, but you can say hello")
}

The advantage is that a when expression is cleaner and easier to understand than a chain of if-else statements.

Summary

In this article we have seen how useful and powerful the when expression is. If you use Kotlin, you will find yourself using the when expression all the time that you can.


Notes

  1. Some languages, like Python, do not have a switch statement at all
8 replies
  1. AD_LB
    AD_LB says:

    But how can I break inside “when” condition?
    I don’t want to have a lot of “if” conditions inside that make the code have a lot of indentation…

    For example:

    when (something){
    someValue->{
    val someValue2=foo()
    if(someTest2 >3)
    break
    // do something with someTest2
    val someValue2=foo()
    if(someTest3 >4)
    break
    // do something with someTest3

    }

    Reply
    • Gabriele Tomassetti
      Gabriele Tomassetti says:

      For this use case there isn’t much special support in Kotlin. To get the cleanest code your best bet might simply be using nested when(s) or to encapsule the code inside each when condition in a function.

      Reply
      • AD_LB
        AD_LB says:

        Nested “when” is the same as indented “if”. Not sure what you mean by the other solution.
        I could use “while(true)” and use the break there, but it would be a bit weird (plus an extra indentation, but at least it’s just once).

        Reply
        • Gabriele Tomassetti
          Gabriele Tomassetti says:

          You could also use a when without an argument in place of while and have many conditions.
          Sorry about the confusing other solution. What I mean is simply that if you don’t like the readability of many ifs inside a when you could hide the code inside functions like this:
          when (something) {
          someValue -> someValueCase()
          someOtherValue -> someOtherValueCase()
          }

          Reply
          • AD_LB
            AD_LB says:

            That’s not always possible, as the conditions can be more sophisticated.
            BTW, I also don’t think that Kotlin supports fallthrough, though I really rarely used it on Java, but when I did, it was useful.

  2. Mitch
    Mitch says:

    The word “when” is temporal in nature and has nothing to do with a conditional. Why did they pick a different name than the perfectly good “switch” keyword and also make such a non-intuitive choice?

    Reply
    • Gabriele Tomassetti
      Gabriele Tomassetti says:

      I do not know why they pick the word “when”, maybe it is true that it was not the right word. However, I think that it was correct to pick a different word from “switch”, because a traditional switch behaves differently, and if you expect the same behavior from when you will get in trouble. So, if you are new to Kotlin, when should stop you in your track and make you think how it works, instead of assuming it is just a traditional switch.

      For instance, with switch you can execute a sequence of branches until you find a break statement, while this is not true for when you always get the first choice. You can use when as an expression, while switch is generally a statement. And so on…

      Reply
    • Chris Graham
      Chris Graham says:

      At least when used as an expression, I found “when” to feel more natural than switch, describing the functional behavior, colloquially.
      eg.
      wakeUpMessage = when (day) {
      in Mon..Fri -> currentCommuteTime()
      Sat -> movieTheaterSchedule()
      else -> inspirationalQuote()
      }

      I did trip over the temporal aspect of “when” (we’re not wiring up an event handler that could be fired at any time something becomes true), but I haven’t come to anything that felt better.

      Reply

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

Your email address will not be published. Required fields are marked *