Using Kotlin's scope functions in not exhaustive with / when

Viewed 106

I'm pretty new with Kotlin and I'm trying to figure out Kotlin's scope functions.

My code looks like this:

with(something) {
 when {
   equals("test") -> var1 = "test123"
   startsWith("test2") -> var2 = "test456"
   contains("test3") -> myNullableVar?.let { it.var3 = "test789" }
  }
}

So before I entered the third check with the .let function my with function does not need to be exhaustive (I'm not returning something, I'm only doing assignments). In my third check I'm using .let as a null-check ... but only for an assignment of it.var3 (if it is not null). I don't need to return anything while I know that Kotlin's .let function returns the result of the body by standard.

Nevertheless now my with/when needs to be exhaustive otherwise it won't compile anymore.

This got me thinking and trying out different things. I found these ways to solve this issue:

  1. I can add an else to my with/when so it becomes exhaustive but actually I don't need an else and I don't want to use it in this case.
  2. I can add another .let, so it looks like this: myNullableVar?.let { it.var3 = "test789" }.let{} .... but this looks kinda hacky to me. Is it supposed to work like this?
  3. Use If(xy==null){...}else{...} stuff but I thought I can solve this with Kotlin differently

Because I'm new with Kotlin I'm not really sure how to handle this case properly. I would probably just go with my second idea because "it works". Or should I don't use .let for null-checks? Add another empty .let{}? Or did I not get the null-safety concept at all? I feel a little bit lost here. Thanks for any help.

1 Answers

This seems to be an unfortunate combination of features…

A when can be non-exhaustive only when it doesn't return a value.  The problem is that the with() function does return a value.  And since the when is at the bottom, its value is what gets returned, so in this case it must be exhaustive.

So why doesn't it insist on an else branch even if you omit the "test3" branch?  That's because assignments don't yield a value.  (They evaluate to Unit, which is Kotlin's special type for functions that don't return a useful value.)  If every branch gives Unit, then Kotlin seems* to be happy to infer a default branch also giving Unit.

But the "test3" branch returns something else — the type of myNullableVar.  So what type does the when infer?  The nearest common supertype of that type and Unit, which is the top type Any?.  And now it needs an explicit else branch!

So what to do?

You've found a few options, none of which is ideal.  So here are a few more, ditto!

  • You could return an explicit Unit from that branch:

      contains("test3") -> { myNullableVar?.let { it.var3 = "test789" }; Unit }
    
  • You could return an explicit Unit from the with():

              contains("test3") -> myNullableVar?.let { it.var3 = "test789" }
          }
          Unit
      }
    
  • You could give an explicit type for the with(). (It has two type parameters, so you'd need to give both, starting with the type of its parameter):

      with<String, Unit>("abc") {
    

I haven't found a single obvious best answer, I'm afraid…

And to answer your last question: yes, ?.let{ is perfectly idiomatic and common for null checks.  In this particular case, replacing it with an if happens to solve the type problem:

contains("test3") -> { if (myNullableVar != null) myNullableVar.var3 = "test789" }

But as well as being long-winded, if myNullableVar is a property and not a local variable, then it opens up a race condition (what if another thread sets it to null in between the test and the assignment?) so the compiler would complain — which is exactly why people use let instead!


(* I can't find a reference for this behaviour.  Is there an official word on it?)

Related