Make return optional

Hello,
Are there any proposals currently lying around to make return keyword optional? I would like to subscribe if there are any.

return still remains a huge pain and a source of numerous bugs for us. It is plain weird that it is optional in lambdas but not in top-level functions.

Thanks!

The return keyword already is optional for functions* in Kotlin.

A source of bugs? That’s surprising since using explicit returns is one of the major things recommend for languages like Groovy (where return is optional everywhere) to prevent bugs.

The reason it’s optional for lambdas is for the use case of short lambdas–it’s practicality for quick actions out waighs the cons of the increased chance of bugs from leaving it out.

For functions, the short use case is well covered by the expression form:

fun getGreeting(): String = "hello world"

fun triple(n: Int) = n * 3

fun prize(place: Int) = where(place) {
    1 -> "gold"
    2 -> "silver"
    3 -> "bronze"
    else -> "none"
}

fun pickPet(allergies: Boolean): Animal = if (allergies) Fish() else Dog()

Longer functions with block bodies make the cost of adding an explicit return small and the benefits of having it high.

You can see this decision in play when you have long lambdas or nested lambdas–where a developer can increase the clearity by explicitly returning from a lambda and possibly adding a label.

If there’s a lot of use cases for block body functions where optional returns can provide some benefit then there might be a case. IMHO I suspect there aren’t any use cases that are not already well covered by the expression function form.

6 Likes

The inconsistency is actually the source of distress and bugs.

In our experience, developers don’t know where to put return. Consider this (option 1):

fun foo(...) {
    // large block
    bar {
          // large block
          return baz
    }
}

versus (option 2):

fun foo(...) {
    // large block
    return bar {
          // large block
          baz
    }
}

Which is extremely common in Kotlin. We found that developers will put return randomly between these two options. So here’s a common pitfall:

  • Developer A implements bar but forgets crossinline.
  • Developer B implements foo which uses bar and places return as in the option 2.

This easily creates a bug and wastes thousands of dollars and precious human lives are lost.

The dilemma doesn’t exist if return is always required (JavaScript):

fun foo(...) {
    // large block
    return bar {
          // large block
          return baz
    }
}

Here they have to pace return at both points. Or, if return is completely optional (ideal Kotlin):

fun foo(...) {
    // large block
    bar {
          // large block
          baz
    }
}

So, inconsistency itself is a huge problem.

Let me point out another example of distress:

fun foo(...) {
    // large block
    return baz
}

So far so good. But let’s wrap it up with bar (again, extremely common in Kotlin):

fun foo(...) = bar {
    // large block
    baz
}

So now it is required to make a change to remove return on a completely unrelated line! This messes up blame big time. If again bar is removed, which is often, then the developer has to go back and see where they need to add return.

I don’t see how any “clarity” is achieved by adding this 6 character word when its place is always at the last expression. Or, isn’t in case of a lambda.

This change would be completely backward compatible since we would only make return optional. If you prefer to use it, fine, and this can be achieved with IDE (“inspections”).

In our experience, developers don’t know where to put return . Consider this

(code)

This easily creates a bug and wastes thousands of dollars and precious human lives are lost.

Could you clarify this further? The two code snippets are functionally the same, both of them will return baz. What exactly is the bug here?

The dilemma doesn’t exist if return is always required (JavaScript)

JavaScript behaves exactly like Kotlin in this regard. ‘standard’ functions require an explicit return, arrow functions (x => x + 1) return the last (and only) statement implicitly. You might wish to choose another language to motivate your argument.

Let me point out another example of distress

(code)

A valid point, although in my own experience you’ll find that most of these scenarios involve wrapping the code in an inline function, where the return statement is equally valid.

1 Like

This actually goes to show that even experienced developers like yourself are easily fooled. Consider if bar is implemented like:

inline fun<T> bar(block: () -> T): T {
    val result = block()
    println("Closing important resources")
    return result
}

Then in option 1 important resources are not closed. They are absolutely not the same! Here is a full example:

	inline fun <T> bar(block: () -> T): T {
		val result = block()
		println("Closing important resources")
		return result
	}

	@Test
	fun testFoo() {
		foo()
	}

	fun foo(): Int {
		bar {
			return 10
		}
	}

return here is a top-level return, not return from a lambda. How confusing!

Only single expression functions do not need return in JavaScript, all others need it. In Kotlin, the situation is quite different.

As the example shows, it is absolutely not valid. It’s a recipe for disaster.

When I teach Kotlin in my company, I spend hours explaining how return works in Kotlin, and everyone is baffled.

There is no other language with such an ominous semantics.

I think the problems you’re discovering with returns are better solved by explicitly marking returns everywhere or taking a second look at the reference page on returns

Making returns optional everywhere would allow for more confusing code as a developer would go from having two options to three–the solution to incorrectly choosing what/when to return is not hiding the return.

2 Likes

I don’t see how you spend hours explaining return in kotlin. return will always return from the nearest fun.
Also I’d argue that your implementation of bar is wrong. The block lambda should have been marked with the crossinline keyword. Maybe there is an argument to be made that crossinline should have been the default and instead there should have been a keyword to allow for non local returns but it’s a bit late to change that. Maybe that could be changed with an optional compiler flag.

5 Likes

Explicitly marking every return would open a whole new can of worms. What if a developer forgets to mark it with @xyz? What if they marked it with a wrong @xyz? Can this really be enforced or caught during code review? How?

Now we have gone from return having a single meaning (albeit inconsistent from a developer perspective) to “one can return from any lambda to any lambda” situation. Not to mention that lambdas are not unique (you can have run inside a run inside a run — which run will return return?).

Pointing to a reference page is not very helpful. I understand how it works. I am pointing to a problem that we are seeing in practice where how it works creates bugs and headaches, and even proposing a backwards compatible solution.

It’s like arguing that comparing null, undefined, false and 0 in JavaScript can be confusing, and a smart guy just points us to a reference page. Thanks but you’re not adding value to the discussion.

I explicitly said in my original post that developer A forgot to add crossinline, so you’re pointing to the obvious here. How many Kotlin developers would think that option 1 and 2 are the same? And isn’t that a problem?

I agree with Wasabi here, the problem doesn’t seem to originate from return but the default behaviour of inline functions. Making returns optional does not fix situations where people would want a early return from a lambda:

fun foo(...) : Code {
    //opens stream and closes it at the end of the function, not made crossinline by accident
    return bar { it: InpustStream 
        //code
        if(condition) return Code.Error //BUG! stream won't close 
        //code
        Code.Okay //okay, bar finished normally
    }
}

I know. What I wanted to say is that return isn’t the real problem here. crossinline is. Your suggestion to change return looks to me like a bandaid over a different problem, without affecting the underlying cause. Sry, if I didn’t express that properly.
Kotlin’s non local return is a powerful feature that allows inline functions to behave as if they are language constructs. Removing return statements or non local returns from the language would break more than it would fix.

5 Likes

Resource clean up should be in a finally block (ideally you can just leverage use) . Even with crossinline there, the definition appears flawed since an exception would result in a resource leak.

Unexpected non local return seems like it’d only cause an issue if you are writing code that is already incorrect and broken for cases involving exceptions.

4 Likes

Wow, yes, that is real confusing to a non Kotlin guru (obviously like myself
)

Without seeing your implementation (and testing it for myself) I would not have followed that logic flow
 I can definitely see how this could get confusing (referring to the example with println(“Closing important resources”).

Thanks for bringing this up
 better hit the reference manual


Is there any scope for an optional compiler warning at least? E.g. This is non local return, are you sure you want it
 or consider marking this param as crossinline
?

1 Like

It’s already an error to return from a crossinline lambda. Here’s the screenshot from IntelliJ, notice how it suggests fixing it by explicitly returning to the lambda with return@bar2


Since returns are pretty important, it’s a compiler error in this case.

@ccfnz as you learn Kotlin, watch how many times a return confusion comes up in practice. I bet most of the confusion points will be solved with explicitly marked returns.

If we’re talking about the specific println(“Closing important resources”) example you’ll see I placed that code in a finally block as chipays mentions. Without it, a caller can easily orphan the resource regardless of return behavior is the following:

// ...
bar { 
    error("Some error...")
    // ...
    return // notice it doesn't matter if bar accidentally early returns or not.
} 

As @Wasabi375 explains, returns aren’t too complicated. We return to the nearest function unless otherwise specified (see the reference page for runnable examples). Unlabeled returns always behave this way and the only way to get a different behavior is by using a labelled return.

The reason it’s not confusing is that the behavior is consistent. Every unlabeled return is the same and you never have to confuse it with a local return.

3 Likes

I guess that is the key, needing to know (or needing to remember) that you need (or at least should consider) crossinline on the block parameter


Also take your point in the example that closing/clearing resources is best in try finally block.

1 Like

Yup.

I can see the desire to lock the returns into one behavior and use some special marker for local returns–I imagine without that it would be confusing. I’m glad it works that way already especially with a compiler error to force stop you from writing confusing returns.

For beginners learning Kotlin, you could just never mention local returns so that they don’t have to worry about it. They’ll use returns the way they expect from whatever language they came from.

Eventually they may see a label on a return and you could explain it’s a nice way to be explicit inside a lambda that they’re used to implicitly returning the last line.

3 Likes