The bottom line is that return
is fine if you’re careful with it, but you shouldn’t use it just 'cause you can. Have some reason.
The problem with return
is that it is nonlocal control flow, and it implements the nonlocality by throwing stackless exceptions and it will try even if the result is absurd.
Take, for instance:
def makeSomething(foo: Foo): Future[Thing] =
Future {
complicatedThing.fold(b){ (acc, x) =>
if (canStopEarly(x)) return Future{ simple(x) }
acc = complex(acc, x)
}
}
The intent seems clear.
This is completely broken.
When you call the method, it will generate the future and happily schedule it for execution later. The method has a hook to catch the exception generated to pass back the return
at the time it’s called, but that’s not when the return happens. It happens later, in some other random thread, while the Future
is executing. The thread, sometime later, dies a gruesome death with a completely uninformative error message since it doesn’t know how to deal with the exception and, with no stack, there isn’t any information to give.
Okay, so, return
can get you into trouble. But what if you’re single-threaded, not catching all exceptions (i.e. no try { stuff() } catch { case t: Throwable => default() }
), and you can really simplify things by using a nonlocal return?
Then do, absolutely! Unless you’re working in a code base where the style is to simply never use them. Like with most things that have observable side-effects, it’s generally safest to use it in contexts where the caller cannot in principle detect what’s happening.
So for instance,
def sumTo100(xs: Seq[Int]): Int =
xs.fold(0){ (sum, x) => if (sum > 100) return 100 else sum+x }
is fine.
You can use breakable/break, but it is implemented using the same mechanism, so it has the same dangers. The advantage is only that you don’t have to define a method to break out of, and since you can define a method anywhere, I don’t really see the point of it.
Note that if you’re trying to stop early, there are other options also that are faster, like
def sumTo100(xs: Seq[Int]): Int = {
def to100(it: Iterator[Int], sum: Int = 0): Int =
if (sum > 100) 100
else if (it.hasNext) to100(it, sum + it.next)
else sum
to100(xs.iterator)
}
These things sometimes avoid the return also; sometimes they don’t. They’re often worth writing when speed not just logic is important. For instance, the non-recursive indexed version is
def sumTo100(xs: Array[Int]): Int = {
var sum, i = 0
while (i < xs.length) {
sum += xs(i)
if (sum > 100) return 100
}
sum
}
which I would consider eminently good code, albeit not very high-level.