I do not see how all that support early returns, IMHO it discourage them even more.
Suppose this contrived example:
/** Returns the half of the sum of the product of each tuple plus one; as long as all values are positive, -1 otherwise. */
def process(input: List[(Int, Int)])): Unit = {
def halfSumTupleProducts(data: List[(Int, Int)]): Int = {
val sumProducts = data.map { case (x, y) =>
val temp = if (x > 0 && y > 0) x * y else return -1
temp + 1
}.sum
sumProducts / 2
}
val result = halfSumTupleProducts(data = input)
println(result)
}
Given your definition of single exit just meaning a function should only return to one place: “he statement immediately following the call”, then where should that return
return to?
Which is the next statement?
- Should it return after the
if / else
and continue with the addition? - Should it return from lambda function inside the
map
and continue themap
processing? - Should it exit the
map
and continue with thesum
? - Should it exit the whole expression and continue with the division?
- Should it exit the whole inner function and continue with the
println
? - Should it exit the whole parent function and continue what its caller was doing even if that function shouldn’t return anything?
- Should it even exit the outer scope where this was called?
- Should it exit this whole call stack until the current thread started?
- Should it exit the whole program?
I known I just started to be ridiculous on purpose but it shows why early returns could be ambiguous in their definition. However, actually, you can think of an exception as an early return and it does exactly that, it can even exit the whole program.
So, why would we really bother about all that when in the 99% of the cases, we can just refactor the code to something like this:
def halfSumTupleProducts(data: List[(Int, Int)]): Int = {
@annotation.tailrec
def loop(remaining: List[(Int, Int)], acc: Int): Int =
remaining match {
case (x, y) :: tail =>
if (x <= 0 || y <= 0) -1 // Early return.
else loop(remaining = tail, acc + (x * y) + 1)
case Nil =>
acc // Normal return.
}
loop(remaining = data, acc = 0)
}
Clear, simple, concise, readable, understandable.
And it does have many exit points, just that all of them are the last expression. (of their corresponding scope).
and what about the 1%
that is not “refactorable” to something like this. Well, there are a couple of alternatives, but I guess using your own exception for controlling the flow and breaking early could be good enough and doesn’t require extra machinery from the language.