Kotlin when: A switch with Superpowers

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

A traditional switch is basically just a statement that can substitute a series of simple if/else that make basic checks. However it cannot replace all sort 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 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 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 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 when always assigns a proper value.

In fact, 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 to possible values, so the two branches cover all cases and 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. Which 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 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 using 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: check 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 accepts 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, does not have a switch statement at all

The post Kotlin when: A switch with Superpowers appeared first on SuperKotlin.