May 09, 2014
The Point of No Return
Alright, every time Martin’s Coursera course runs we get people in #scala asking why they get style points taken off for using return
. So here’s the pro tip:
The
return
keyword is not “optional” or “inferred”; it changes the meaning of your program, and you should never use it.
Let’s look at a little example.
// Add two ints, and use this method to sum a list
def add(n:Int, m:Int): Int = n + m
def sum(ns: Int*): Int = ns.foldLeft(0)(add)
scala> sum(33, 42, 99)
res0: Int = 174
// Same, using return
def addR(n:Int, m:Int): Int = return n + m
def sumR(ns: Int*): Int = ns.foldLeft(0)(addR)
scala> sumR(33, 42, 99)
res1: Int = 174
So far so good. There is no apparent difference between sum
and sumR
which may lead you to think that return
is simply an optional keyword. But let’s refactor a bit by inlining add
and addR
.
// Inline add and addR
def sum(ns: Int*): Int = ns.foldLeft(0)((n, m) => n + m) // inlined add
scala> sum(33, 42, 99)
res2: Int = 174 // alright
def sumR(ns: Int*): Int = ns.foldLeft(0)((n, m) => return n + m) // inlined addR
scala> sumR(33, 42, 99)
res3: Int = 33 // um.
What the what?
So, the short version is:
A
return
expression, when evaluated, abandons the current computation and returns to the caller of the method in whichreturn
appears.
So in the example above, the return
statement in the anonymous function does not return from the function it appears in; it returns from the method it appears in. Another example:
def foo: Int = {
val sumR: List[Int] => Int = _.foldLeft(0)((n, m) => return n + m)
sumR(List(1,2,3)) + sumR(List(4,5,6))
}
scala> foo
res4: Int = 1
Non-Local Return
When a function value containing a return
statement is evaluated nonlocally, the computation is abandoned and the result is returned by throwing a NonLocalReturnControl[A]
. This implementation detail escapes into the wild without much ceremony:
def lazily(s: => String): String =
try s catch { case t: Throwable => t.toString }
def foo: String = lazily("foo")
def bar: String = lazily(return "bar")
scala> foo
res5: String = foo
scala> bar
res6: String = scala.runtime.NonLocalReturnControl
To those who say well you should never catch Throwable
anyway, I say well you shouldn’t be using exceptions for flow control. The breakable { ... }
nonsense in stdlib uses a similar technique and similarly should not be used.
Another example. What if a return
expression is captured and not evaluated until after the containing method has returned? Well you now have a time-bomb that will blow up whenever it’s evaluated.
scala> def foo: () => Int = () => return () => 1
foo: () => Int
scala> val x = foo
x: () => Int = <function0>
scala> x()
scala.runtime.NonLocalReturnControl
And as an extra bonus NonLocalReturnControl
extends NoStackTrace
so you are given no clue about where the bomb was manufactured. Good stuff.
For this reason, if you make a choice to use return
, you should practice safe returns and use -Xlint:nonlocal-return
, available with 2.13:
scala> def foo: () => Int = () => return () => 1
^
warning: return statement uses an exception to pass control to the caller of the enclosing named method foo
foo: () => Int
However, the safest practice remains abstinence.
What is the type of a return expression?
In return a
the returned expression a
must conform with the return type of the method in which it appears, but the expression return a
itself also has a type, and from its “abandon the computation” semantics you can probably guess what that type is. If not, here’s a progression for you.
def x: Int = { val a: Int = return 2; 1 } // result is 2
Well this typechecks so our guess might be that the type of return a
is the same as the type of a
. So let’s test that theory by trying something that shouldn’t work.
def x: Int = { val a: String = return 2; 1 }
Hmm, that typechecks too. What’s going on? Whatever the type of return 2
is, it conforms with both Int
and String
. And since both of those classes are final and Int
is an AnyVal
you know where this is headed.
def x: Int = { val a: Nothing = return 2; 1 }
Right. So, whenever you encounter an expression of type Nothing
you would do well to turn smartly and head the other direction. Because Nothing
is uninhabited (there are no values of that type) you know that the expression has no normal form; when evaluated it must loop forever, exit the VM, or (behind door #3) abruptly pass control elsewhere, which is what’s happening here.
If your reaction is “well logically you’re just invoking the continuation, which we totally do all the time in Scheme so I don’t see the problem” then fine. Cookie for you. The rest of us think it’s insane.
Return is not referentially transparent.
This kind of goes without saying, but just in case you’re not sure what this means, if I say
def foo(n:Int): Int = {
if (n < 100) n else return 100
}
then I should be able to rewrite my program thus, with no change in meaning
def foo(n: Int): Int = {
val a = return 100
if (n < 100) n else a
}
which of course doesn’t work. Evaluating a return
expression is a side-effecting operation.
But what if I really need it?
You don’t. If you find yourself in a situation where you think you want to return early, you need to re-think the way you have defined your computation. For example:
// Add up the numbers in a list, up to 100 max
def max100(ns: List[Int]): Int =
ns.foldLeft(0) { (n, m) =>
if (n + m > 100)
return 100
else
n + m
}
can be rewritten using simple tail recursion:
// Add up the numbers in a list, up to 100 max
def max100(ns: List[Int]): Int = {
def go(ns: List[Int], a: Int): Int =
if (a >= 100) 100
else ns match {
case n :: ns => go(ns, n + a)
case Nil => a
}
go(ns, 0)
}
This is always possible. Eliminating return
from the Scala language would result in zero programs that could no longer be written. It may take a bit of effort to get into the mindset, but in the end you will find that writing computations that terminate properly is far easier than trying to reason about side-effects manifested as nonlocal flow control.