31

I saw the following comment in a S.O. post, and I'm intrigued:

why don't you use if for null checks? a?.let{} ?: run{} is only appropriate in rare cases, otherwise it is not idiomatic – voddan May 15 '16 at 7:29 best way to null check in kotlin?

Why is that construct "only appropriate in rare cases"?
The lead engineer for Kotlin says,

run allows you to use multiple statements on the right side of an elvis operator https://stackoverflow.com/a/51241983/6656019

although I admit that's not actually endorsing it as idiomatic. Both of these posts seem to be from very well respected S.O. Kotlin contributors.
The post that inspired the original comment mentions that the let part of the expression is important if a is mutable. In that case, you'll need a?.let{} ?: run{} instead of if{} else {}.

I find I like the "let Elvis run" construct. Should I avoid it in most cases?
Thanks for any insight.

Andy Marchewka
  • 412
  • 1
  • 5
  • 8

2 Answers2

35

It's dangerous to conflate foo?.let { bar(it) } ?: baz() with if (foo != null) bar(foo) else baz().

Say you have a function: fun computeElements(): List<Int>? = emptyList()

Consider this code:

val maxElement = computeElements()?.let { it.max() } ?: return
println("Max element was $maxElement")

Compared to:

val list: List<Int>? = computeElements()
val maxElement = if (list != null) list.max() else return
println("Max element was $maxElement")

You may think these are two equivalent forms. However, if you run both, you'll see that the former does not print anything to stdout!

This is because it.max() returns null for an empty list (because there is no max element), which causes the right-hand side of the Elvis expression to be evaluated, and so the function returns early.

In short, ?.let { ... } ?: ... allows both branches of the "if-else" to be evaluated, which is dangerous. Aside from this form not being readable (if-else is universally understood, while let-run is not), subtle bugs can occur.

kevinmost
  • 565
  • 1
  • 5
  • 7
  • 1
    Great caveat, and great illustration of @mhswtf's comment. I'm looking at your example and thinking that the early return is what I'd want there, anyway. But your point is very well taken that you have to consider the return value of the lambda that you pass to let. – Andy Marchewka Mar 12 '19 at 21:28
  • Why do `it.max()` and `list.max()` return different values? Aren't they both evaluated to be operated on the same list instance when non-null? – Samuel Neff Feb 22 '22 at 15:01
  • It should be noted that using `?.also { }` instead of `?.let { }` prevents the problem you describe in your last paragraph about both branches of the if-else statement being evaluated at the cost of not getting the result of what is inside of the let lambda. – xdevs23 Jan 23 '23 at 16:17
17

In that case, you'll need a?.let{} ?: run{} instead of if{} else {}

No, you can omit the run part of run { statement } and use a?.let{} ?: statement.

Should I avoid it in most cases?

You should use it when you need it. E.g. when you want to run multiple statements in that scenario. It is pointed out that that is a rare scenario. Often you will see just a single statement on the right hand side of an elvis operator.
And of course don't use it when you don't need it. Keep the code simple.

Tim
  • 41,901
  • 18
  • 127
  • 145
  • 6
    `run` basically makes sure the elvis operator is useable where the statement run after the null check fails is over one statement (which is useful if there's a couple lines of code involved after the null check) – Zoe Sep 27 '18 at 14:52
  • @Zoe that is what I meant. I think you worded it better – Tim Sep 27 '18 at 14:59
  • 1
    Thanks, all. Very glad to hear it's acceptable. I did not mean to imply I wanted to use it when a simple statement will do, of course. I was really hoping for some debate about when "let Elvis run" is not idiomatic, though, to understand what @voddan was originally getting at. – Andy Marchewka Sep 29 '18 at 12:07
  • 13
    Remember that there's a caveat to this. The statement after the elvis operator runs if whatever the let block returns is null, so both let and run can be executed in this case. – mhswtf Feb 18 '19 at 11:14
  • @mhswtf *both let and run can be executed* - true, but the Q and A here do not imply they cannot – Tim Feb 18 '19 at 11:20
  • 1
    @TimCastelijns I know, it was merely a heads up. One might expect it to act exactly like an if != null else. – mhswtf Feb 18 '19 at 14:47
  • 2
    @mhswtf oh yeah good point. I upvoted your comment for visibility – Tim Feb 18 '19 at 14:50