Puzzler 14

Return to Me!

Unlike Java, Scala does not require methods returning a value to contain an explicit return statement. If omitted, the method will inherently return the result of the last expression.

Occasionally, though, explicit return statements do show up in Scala code. In fact, a single method can contain multiple return expressions, as in the following program. What does it do?

  def sumItUp: Int = {
    def one(x: Int): Int = { return x; 1 }
    val two = (x: Int) => { return x; 2 }
    1 + one(2) + two(3)
  }
  
println(sumItUp)

Possibilities

  1. Prints:
      3
    
  2. Prints:
      4
    
  3. Prints:
      6
    
  4. Fails to compile with an error: unreachable code.

Explanation

Something that immediately stands out in this code are the number literals, 1 and 2, which appear after return statements. Given their location in the code, they will never be reached. The compiler will likely detect this and fail with an unreachable code error. Therefore, candidate answer number 4 is the correct one, right?

Not so fast! The Scala Language Specification stipulates that anything following a return expression is not evaluated, i.e., simply ignored.[1]

The unreachable literals 1 and 2 thus turn out to be a red herring. With those out of the way, surely some common sense can help identify the right answer. Both method one and function value two return the arguments passed to them, i.e., 2 and 3, respectively. Therefore, the result is 6, so the third candidate answer must be the correct one.

This reasoning can easily be verified in the REPL:

  scala> println(sumItUp)
  3

Hmm...not the expected value 6. The correct answer is number 1! Let's go back to the code to see what we overlooked. The function literal defined for value two looks a bit odd because of the return statement. Was it a mistake to assume that the return keyword has the same semantics in a function body as in a method? Once again, the REPL is your friend:

  scala> val two = (x: Int) => { return x; 2 }
  <console>:7: error: return outside method definition
         val two = (x: Int) => { return x; 2 }
                                 ^

Indeed, there is more to return than expected! Time to consult the language specification again:[2]

A return expression return e must occur inside the body of some enclosing named method or function [f]. ... The return expression evaluates the expression e and returns its value as the result of f.

The enclosing named method for the first return x statement is method one. However, the enclosing named method for the second return x statement is not the function value two, but method sumItUp. And that is the essence of the puzzler. It might appear as if function value two should qualify as an enclosing named function, but only methods and local functions (defs) can act as the enclosing scope of return statements. So, when 3 is applied to function value two, it is immediately returned as a result of the entire method sumItUp. The result of the invocation of method one is ignored.

Discussion

Idiomatic Scala coding style does not encourage explicit return expressions and, in particular, methods with multiple return statements. This favors more concise, less complex methods.

Since use of explicit return statements is discouraged, you might be wondering if it is necessary to use them at all in a Scala program. After all, most of the time, explicit return statements can be replaced with equivalent expressions. As nearly all control structures in Scala are expressions, this is not difficult. For example, consider the following method:

  def fibonacci(n: Int): Int = {
    if (n < 2return n
    fibonacci(n - 1) + fibonacci(n - 2)
  }

This method can be rewritten without return like this:

  def fibonacci(n: Int): Int =
    if (n < 2) n
    else fibonacci(n - 1) + fibonacci(n - 2)

Your code can be rewritten in this way even if you want to return early from multiple places, when checking whether preconditions to your function are satisfied, for example. Those cases can be implemented as nested if-else expressions. By doing so, however, the normal path of execution can become less obvious.[3] In such situations, cutting the execution of a method short with a return can make the code more readable. Additionally, explicit return statements can be used for performance optimization, to escape out of a tight loop, for instance.

A return expression is required, however, to break out of multiple levels of nested functions. In such cases, the control flow has to jump out of all nested functions to the innermost enclosing method (i.e., the first method around the return, as opposed to the innermost enclosing block).[4]

Suppose you wanted to implement a method that, for a given sequence of currencies, queries a web service to get the current exchange rate and returns the first currency whose rate change has exceeded a given threshold since the last query. Here is how that method might look:

  def findHotCurrency[A](currencies: Seq[A], 
      threshold: (DoubleDouble) => Boolean): Option[A] = {
    for (currency <- currencies) {
      val oldRate = getCurrentRate(currency)
      val newRate = fetchRate(currency)
      if (threshold(oldRate, newRate)) return Some(currency)
    }
    None
  }

Note that the return statement in this example is in fact contained inside a nested function! Namely, since the Scala compiler translates a for expression:

  for (x <- expr) body

into:

  expr foreach (x => body)

the body of the for becomes the body of a nested function that is passed to foreach. That also means that if you didn't explicitly return, as in:

  if (threshold(oldRate, newRate)) Some(currency)

the code would still compile, but the method would incorrectly return None because the result of the function passed to foreach is always discarded.

The only other way to implement the same control flow is to throw and then catch an exception. As a matter of fact, returning from a nested anonymous function is implemented (by the compiler) by throwing and catching a scala.runtime.NonLocalReturnControl exception. This is why it is not a good idea to catch java.lang.Throwable in Scala: doing so might interfere with the control flow of return statements used in function literals.

Exception handling is, in general, an expensive operation. For that reason, return statements should be avoided in lambda functions in performance critical code, if possible.

Lastly, unreachable code (the fourth candidate answer) is actually a warning that's relevant only in the context of pattern matching, where a pattern can match everything and thus prevent subsequent cases from ever being reached.[5]

By default, the Scala compiler does not complain about other unreachable code. If desired, the -Ywarn-dead-code compiler option will warn you:

  scala> def sumItUp: Int = {
           def one(x: Int): Int = { return x; 1 }
           val two = (x: Int) => { return x; 2 }
           1 + one(2) + two(3)
         }
  <console>:8: warning: dead code following this construct
           def one(x: Int): Int = { return x; 1 }
                                    ^
  <console>:9: warning: dead code following this construct
           val two = (x: Int) => { return x; 2 }
                                   ^
  sumItUp: Int

You can also take it one step further and use the -Xfatal-warnings flag, which will cause compilation to fail if there are any warnings.

image images/moralgraphic117px.png If possible, you should avoid using explicit return statements. If you need to use them, be conscious of the context and ensure the execution will resume where you intend. Be aware that return statements return only from methods and local functions, not from function values that may be defined within them.

Footnotes for Chapter 14:

[1] Odersky, The Scala Language Specification, Section 6.20. [Ode14]

[2] Odersky, The Scala Language Specification, Section 6.20. [Ode14]

[3] Fowler, Refactoring: Improving the Design of Existing Code. [Fow99]

[4] Griffith, "Purpose of return statement in Scala." [Gri10]

[5] See Puzzler 2 for a more detailed discussion of pattern matching.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.21.106.7