Assignment to variable inside when block


#1

Say, I have multiple functions, each returns a sealed class and I want to chain-check the results using when block:

sealed class Handled {
  data class Yes(val result: Boolean) : Handled()
  object No : Handled()
}

fun getResult(): Handled {
 // some logic to get the result
}

fun getDifferentResult(): Handled {
 // some other logic but output is of Handled type
}

when {
  getResult() is Handled.Yes -> // here I want to get the associated value of Yes (result) and I can't without first assigning it to a value 
  getDifferentResult() is Handled.Yes -> // same as above
  else -> //
}

Now, I know I can assign the result of a function to a variable and then use it in when block, like so:

val output01 = getResult()
val output02 = getDifferentResult()
when {
  output01 is Handled.Yes -> print(output01.result)
  output02 is Handled.Yes -> print(output02.result)
  else -> print("not handled still")
}

But the above, first of all, is not very nice looking - I don’t use the variables anywhere else but inside when.

What’s more important, is that I know that execution of when’s branches is lazy, so if one matches, the other ones are not executed. Meaning, if I would match on getResult() in my first example, the getDifferentResult() would not be called, which is excellent. Sadly, I can’t achieve the same using the second example. I probably could do something like val output01: Handled by lazy {} but that’s even less readable.

Is there some other way to achieve what I want? In my mind, being able to assign the result to a variable inside the block would solve the problem, similarly to how it’s possible when matching cases in Swift:

switch getResult() {
  case let .Yes(result):
    print(result)
  default:
    print("not handled still")
}

#2

I think you would better add one more layer of abstraction and move the when block to a function that you’d call for each result:

fun main(args: Array<String>) {  
    val results = listOf(getResult(), getDifferentResult(), getUnhandled())
    results.forEach(::printResult)   
}

fun printResult(result: Handled) {
    when(result) {
        is Handled.Yes -> println(result.result)
        else -> println("not handled still")
    }
}

sealed class Handled {
  data class Yes(val result: Boolean) : Handled()
  object No : Handled()
}

fun getResult(): Handled = Handled.Yes(true)

fun getDifferentResult(): Handled = Handled.Yes(false)

fun getUnhandled() = Handled.No

This prints:

true
false
not handled still

#3

But yours does something totally different from what I was trying to achieve. Your code runs all the functions and prints all their outputs. Mine would only print the one that matched, not even calling anything down the line from the matched branch.

I used a very simplified example, in reality, each function could be doing something expensive, so I would want to find the first function output that matches my condition, do something with the result of that match and not even call the other functions

Something like this:

fun getResult(): Handled = Handled.Yes(true)

fun getUnhandled() = Handled.No

when {
  getResult() is Handled.Yes(val result) -> // do one thing with result
  getUnhandled() is Handled.Yes(val result) -> // do another thing with result
  // many
  // many
  // other
  // functions
}

In the above getUnhandled() would never be called, as the first branch matches the condition


#4

Now I understand …

What do you think about this approach using a sequence of function references:

fun main(args: Array<String>) {  
    val candidates = sequenceOf(::getResult, ::getDifferentResult, ::getUnhandled)
    val first = candidates.map{ it() }.first{ it is Handled.Yes } 
    printResult(first)
}

fun printResult(result: Handled) {
    when(result) {
        is Handled.Yes -> println(result.result)
        else -> println("not handled still")
    }
}

sealed class Handled {
    data class Yes(val result: Boolean) : Handled()
    object No : Handled()
}

fun getResult(): Handled{
    println("called getResult")
    return Handled.Yes(true)
}

fun getDifferentResult(): Handled {
    println("called getDifferentResult")
    return Handled.Yes(false)
}

fun getUnhandled(): Handled {
    println("called getUnhandled")
    return Handled.No
}

This prints only

called getResult
true

#5

This is very nice indeed!

I can see myself using something like in some of the situations, but definitely not all - as I tried to show in the code comments in my last message, the handling of the result can be totally different, depending on which branch has matched. In your example, the result is always handled the same (by printResult).

Oh, and, of course, the functions I call - they can use completely different arguments: getResult(someStringArg), getDifferentResult(someIntArg)

Also, the sealed class can be much more complex. I have Yes No, but it can have many different states and completely different associated values.:

when {
   getA() is SealedClass.StateA(val a, val b) -> // do something with a and b
   getB() is SealedClass.StateB(val a) -> // do something with a
}

#6

May it’s best to use the good, old if.

But I’m wondering if it could make sense to combine the action and the handler in an object. Something like this:

class Action(
    private val f: () -> Handled,
    private val condition: (Handled) -> Boolean, 
    private val handler: (Handled) -> Unit
) {
    operator fun invoke() {
        val result = f()
        if(condition(result) {
        	handler(result)
        }
    }
}

However it is just a glorified if at the moment, but maybe it could bring you to an idea …


#7

Even with if/else in Swift it looks so much more elegant:

enum Handled {
  case yes(result: Bool)
  case batman(result: Bool, name: String)
  case no
}

func getA() -> Handled {
  return .yes(true)
}

func getB() -> Handled {
  return .no
}

func getC() -> Handled {
  return .batman(result: true, name: "Bruce")
}

if case let .yes(result) = getA() {
  print(result)
} else if case .no = getB() {
  print("dsfdsf")
} else if case let .batman(result, name) = getC() {
  if result {
    print("Batman's real name is [\(name)]")
  }
}

Ability to assign the result of matched case during the matching is something I really would love to see in Kotlin - especially in the when expressions.


#8

There is feature request about when statement: https://youtrack.jetbrains.com/issue/KT-4895


#9

A rather nasty “hack” would make this work:

inline fun ifHandled(result: Handled, action:(Boolean)->Unit): Boolean = when(result) {
  is Yes -> { action(result.result); true }
  is No -> false
}

// then the usage site looks like:
when {
  ifHandled(getResult) { /*some logic*/ } -> Unit
  ifHandled(getDifferentResult) { /*some other logic*/ } -> Unit
  else -> // whatever
}

Instead of the when you could also just use a shortcircuit or expression. The key is that the inline function provides the action. This inline function could even return from the function (using a helper function that way is probably the cleanest way to allow a return value from the chain).