"when" lacks the important attribute "fallthrough" of "switch"

For example, in Java switch case, there is a difference between having break and not having break
However, Kotlin’s when lacks this function and cannot achieve the effect of example code in Java

// Example code

int i = 0;
switch(i){
     case 0:
          System.out.println("0");
     case 1: 
          System.out.println("1");
          break;
     case 2: 
          System.out.println("2");

// The result is shown as:
// 0
// 1

Yes it does and many people (me included) will argue that this is a good thing since this is a big cause of bugs and a rarely used feature.

6 Likes

I also don’t miss this feature at all. It only caused me troubles in the past, because accidentally I forgot to put break somewhere.

As for falling through feature you could still achive a quite similar effect by writting something like this:

val i = 0
when (i) {
	0, 1 -> {
		if (i == 0) {
			println(0)
		}
		println(1)
	}
	2 -> println(2)
}
1 Like

I am rewriting a Go program with Kotlin.
So now I have to use a lot of if to solve the problem, because it has a lot of case parts, and use penetration function

// Go code

switch {
	case level >= 98:
		score += (level- 98) * 5200
		fallthrough
	case level>= 97:
		score += (level - 97) * 1500
		fallthrough
	case level>= 95:
		score += (level- 95) * 100
		fallthrough
	case level>= 94:
		score += (level- 94) * 200
		fallthrough
	case level>= 92:
		score  += (level- 92) * 100
		fallthrough
	...
}

// Kotlin code
        if (level>= 98){
            score += (level- 98) * 5200
        }
        if (level>= 97){
            score += (level - 97) * 1500
        }
        if (level>= 95){
            score += (level- 95) * 100
        }
        if (level>= 94){
            score += (level- 94) * 200
        }
        if (level>= 92){
            score  += (level- 92) * 100
        }
        ...

I know the feeling of porting code from one language to another. There are often language features that look so similar but have a small difference that makes porting code really annoying. That said I don’t mind the if statements in this example. Maybe it depends on the number of them but if it’s to many to comfortably fit on one screen maybe you should think about other ways to simplify that algorithm.

1 Like

(By the way, this behaviour is normally called ‘fallthrough’; that word would make this thread a bit easier to follow.⠀ ‘Penetration’ has… other associations in English.)

3 Likes

Maybe it would be good to focus on use cases that don’t all break (already covered by when) or all fallthrough (better covered by if-statements IMO)

I suspect allowing the classic style of break/fallthrough will fall for the common trap of obfuscating the jumps between cases like it does in many other languages. Personally, I prefer to refactor to allow some nesting to organize jumps instead of requiring others to do my mental math and play through each case in their head as they watch for breaks or fallthroughs.

Maybe there’s another syntax change that could better solve the issue (if it’s a big enough issue) that doesn’t hide jumps the way break/fallthrough does?

Thank you for your comments, which have now been corrected

You may replace this code with something like :

listOf(Pair(98, 5200), Pair(97, 1500), Pair(95, 100))
        .filter { level >= it.first }
        .forEach {
            score += (level - it.first) * it.second
        }

I like the rewrite. Here’s more edits with more names in case it makes it more clear.

data class LevelScore(val level: Int, val multiplier: Int) {
    fun getScore(currentLevel: Int): Int = if (currentLevel >= level) {
        (currentLevel - level) * multiplier
    } else 0
}

fun main() {
    val myLevel = 95 // Pick some level
    var score = 0 // Pick some starting score

    val levels = listOf(
        LevelScore(98, 5200),
        LevelScore(97, 1500),
        LevelScore(95, 100),
        LevelScore(94, 200),
        LevelScore(92, 100),
    )

    score += levels.sumBy { it.getScore(myLevel) }
    println("Score = $score")
}
1 Like

You can also view it as folding the list of scoring rules:

fun calculateScore(level: Int) = listOf(Pair(98, 5200), Pair(97, 1500), Pair(95, 100))
    .fold(0) { intermediateScore, scoringRule -> 
        intermediateScore + if (level >= scoringRule.first)
            (level - scoringRule.first) * scoringRule.second 
        else 
            0
    }

The scoring rules should be placed in a constant, but I skipped that for brevity.

2 Likes

And just to show how awesome Kotlin is, make it an extension function of a list of integer pairs:

fun List<Pair<Int, Int>>.calculateAnyScore(level: Int) = 
    fold(0) { intermediateScore, scoringRule -> 
        intermediateScore + if (level >= scoringRule.first)
            (level - scoringRule.first) * scoringRule.second 
        else 
            0
    }

fun calculateScore(level: Int) = listOf(Pair(98, 5200), Pair(97, 1500), Pair(95, 100))
    .calculateAnyScore(level)

And your code is shorter than your original Go code, easier to understand, and reusable.

2 Likes

Even better @jstuyts.

Here’s the steps all collected into runnable examples (looks like runnable examples don’t work when hidden inside a details block):

Imperative
fun main() {
    val scoringRules = mapOf(98 to 5200, 97 to 1500, 95 to 100)
    var score = 0
    val level = 97

    for ((ruleLevel, multiplier) in scoringRules) {
        score += if (level >= ruleLevel) (level - ruleLevel) * multiplier else 0
    }

    println(score)
}
Functional
fun main() {
    val scoringRules = mapOf(98 to 5200, 97 to 1500, 95 to 100)
    var score = 0
    val level = 97

    // You could use fold, reduce, or other operations here. I chose sumBy since it's easier for those not familier with function coding and we don't care about doing anything fancy with the accumulator
    score += scoringRules.entries.sumBy { (ruleLevel, multiplier) ->
        if (level >= ruleLevel) (level - ruleLevel) * multiplier else 0 
    }

    println(score)
}
Functional and refactored into an extension function
fun main() {
    val scoringRules = mapOf(98 to 5200, 97 to 1500, 95 to 100)
    var score = 0
    val level = 97

    score += scoringRules.calculateScore(level)

    println(score)
}

fun Map<Int, Int>.calculateScore(level: Int) = entries.sumBy { (ruleLevel, multiplier) ->
    if (level >= ruleLevel) (level - ruleLevel) * multiplier else 0 
}

EDIT: Removed a few of the examples

3 Likes

IMO the functional aproach should use filter befor summing the results. It’s easier to read that way because you can get ride of the if-else expression. So it should be

scoringRules.entries.filter { it.first >= level }.sumBy { (level - it.second) * multiplier }

But I accept that this is my personal preference and both solutions are equally valid. Also based on the optimizations done by the JVM (or other target platform) your solution might be a bit faster because filter creates a new list of items but I think most modern JVMs might be able to optimize this away because the list is only used once directly after creating it.

1 Like