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 which return 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.