Non-local returns in Scala 3.2

I just upgraded to Scala 3.2 and got a rude surprise: over 100 warnings that non-local returns are no longer supported. I had no idea that I had so many of them. So I did a quick search to figure out what to do about them, but I did not come up with any good explanations with examples. Can anyone provide a trivial example of how to deal with them – or point me to a good explanation with examples? Thanks.

This might be helpful tpolecat

4 Likes

If a non-local return is not really necessary then it’s better to avoid them. But if you do really need it, Scala 3 added library-level non-local returns, kind of like break in Scala 2. Deprecated: Nonlocal Returns

3 Likes

I am going through my code and trying to restructure the non-local returns to eliminate the warnings in Scala 3.2.

I came across the following example in my code. I have a function that schedules arriving flights to a runway and computes a delay maneuver if necessary to maintain the minimum required spacing between arrivals. The first step of the function is to check for gaps in the arrival stream and try to fit the flight into a gap if one can be found that is large enough. Here is the first line of the function:

for maneuver <- tryArrivalGaps(traj) do return maneuver

The tryArrivalGaps function returns an Option, and this line just returns the value in the option if it is non-empty. But this is a non-local return, so I restructured it to this:

val gapManOpt = tryArrivalGaps(traj) // arrival gap maneuver option
if gapManOpt.nonEmpty then return gapManOpt.get

This is a much less elegant syntax to get around the prohibition of non-local returns. Is there a better way? Yes, I know I can use throwReturn, but that seems to me like even more of a kludge.

The usual no-returns way to do it is something like:

tryArrivalGaps(traj) match {
  case Some(maneuver) => maneuver
  case None => {
    // Do the non-gap version
  }
}

Or probably better for this case, simply:

tryArrivalGaps(traj).getOrElse {
  // Do the non-gap version
}

Both of these approaches are bog-standard – I use both frequently.

Basically, there is almost never a need for return – I literally never, ever, use it. You just have to be willing to nest your code a little, or break the inner bits out to a separate function.

1 Like

Quick plug for -Xlint

scala> def f(xs: List[Int]): List[Int] = xs.map(i => if (i < 0) return Nil else i)
def f(xs: List[Int]): List[Int]

scala> :replay -Xlint
replay> def f(xs: List[Int]): List[Int] = xs.map(i => if (i < 0) return Nil else i)
                                                                 ^
        warning: return statement uses an exception to pass control to the caller of the enclosing named method f
def f(xs: List[Int]): List[Int]

I started converting some of my non-local returns to use “returning” and “throwReturn”. I am wondering why this feature was added. I understand that just using “return” for non-local returns can be inefficient, but isn’t it possible for the compiler to simply identify the return as non-local and treat it the same as if “throwReturn” were used?

I have been experimenting a bit with the use of “returning” and “throwReturn.” Here is an example:

  def indxOf1stOnsidePt: Int = returning {
    for (waypt, indx) <- waypts.zipWithIndex do
      if onside(waypt.pos) then throwReturn(indx)
    0
    }

I found that it still compiles if I reduce the scope of the “returning” section as follows:

  def indxOf1stOnsidePt: Int =
    for (waypt, indx) <- waypts.zipWithIndex do
      if onside(waypt.pos) then returning { throwReturn(indx) }
    0

Is that second form equivalent to the first form?

Before 3.2 non-local returns were implemented exactly the same as throwReturn: by throwing an exception. The reason throwReturn exists now is because it’s explicit.

returning { throwReturn(indx) } is identical to only typing indx, i.e. the throwReturn returns to its enclosing returning method call. In your second example that means the loop will continue iterating.

putting returning { ... } around the whole method body means that throwReturn(indx) will immediately exit the for loop because the next part of code to be executed will be after the block, which means exiting the method.

1 Like

Before 3.2 non-local returns were implemented exactly the same as throwReturn: by throwing an exception. The reason throwReturn exists now is because it’s explicit.

OK, thanks. Does that mean there is a significant performance penalty compared to using a normal return (i.e., without using the “return” keyword)?

e.g. in 3.0.2 we have:

def indexOf(xs: List[Int], target: Int): Int = {
  for ((x,i) <- xs.iterator.zipWithIndex) {
    if (x == target) return i
  }
  -1
}

compiles to equivalent of

def indexOf(xs: List[Int], target: Int): Int = {
  val labelControl = new Object();
  try {
    for ((x,i) <- xs.iterator.zipWithIndex) {
      if (x == target) throw new NonLocalReturnControl[Int](labelControl, i)
    }
    -1
  } catch {
    case ex: NonLocalReturnControl[Int] if ex.key == labelControl =>
      ex.value
  }
}

the NonLocalReturnControl exception itself does not fill in the stack trace, so the performance impact of creating + throwing is minimal

however (my hypothesis) if you have a lot of code that catches Throwable, e.g. using the NonFatal extractor, then this could have a penalty as it will intercept NonLocalReturnControl before rethrowing it

I have another question on this topic, so I’ll append it to this earlier discussion.

I’m not particularly enamored with the idea of forcing non-local returns to be explicit, but I’ll leave that for another time. One thing that bothers me just a bit about it is that it seems to force me back to using braces for functions. I have a lot of these. For example,

def avoidsOtherAirports: Bool = returning {
  for (code, zone) <- airportZones if code != airportCode do
    if entersZone(zone) then throwReturn(false)
  true
  }

There doesn’t seem to be any way to eliminate the braces here, but I just want to be sure I’m not missing something. Is there some trick to avoid them, or am I stuck with them? Thanks.

As of Scala 3.3.0-RC2 (which just came out this week), the old fewerBraces option is now fully part of the language and available by default:

3.3.0-RC2 also includes the following late-breaking (ha ha ha, pun intended) redesigned and renamed replacement for nonlocal returns:

so one can now, for example:

Welcome to Scala 3.3.0-RC2 (17.0.6, Java OpenJDK 64-Bit Server VM).

scala> import scala.util.boundary, boundary.break

scala> def myFind[T](xs: Iterable[T], pred: T => Boolean): Option[T] =
     |   boundary:
     |     for x <- xs do
     |       if pred(x) then break(Some(x))
     |     None
     | 
def myFind[T](xs: Iterable[T], pred: T => Boolean): Option[T]
                                                                                                    
scala> myFind(List.range(0, 20), _ > 10)
val res3: Option[Int] = Some(11)
                                                                                                    
scala> myFind(List.range(0, 20), _ > 100)
val res4: Option[Int] = None

3.3.0-RC2 is of course just a release candidate, so you may wish to wait until 3.3.0 final is out and all the tooling you use supports it.

1 Like

Hey, that’s great! I assume it will have continue as well. And in most cases it should eliminate the need for returning and throwReturn.

It doesn’t have continue.

It does have a feature I didn’t show: named boundaries.

And the named boundaries can be used to express both Java’s break and continue. Also normal, local returns.

I’d like to see a continue-like example. (Though if nobody steps forward, I could probably come up with one myself…)

To answer my own question, the equivalent of continue is to use break, but to put the boundary inside the loop, rather than outside.

So for example:

scala> import util.boundary, boundary.break
     | def myFilter[T](xs: List[T], pred: T => Boolean): List[T] =
     |   val result = collection.mutable.ListBuffer.empty[T]
     |   for x <- xs do
     |     boundary:
     |       if !pred(x) then break()
     |       result += x
     |   result.toList
def myFilter[T](xs: List[T], pred: T => Boolean): List[T]
                                                                                                    
scala> myFilter(List.range(0, 10), _ % 2 == 0)
val res0: List[Int] = List(0, 2, 4, 6, 8)

It’s not a compelling example, since it can be written cleanly without using boundary/break at all. But it illustrates the idea regardless, I hope.

Maybe someone has a pet example of a continue-based loop they’d like to attempt to translate.

2 Likes

Here’s an example using named boundaries:

scala> boundary[Int] { outer ?=>
     |   var i = 1
     |   val b = boundary {
     |     if i*i > i then boundary.break(false)
     |     else if { i += 1; i*i < 9 } then boundary.break(i)(using outer)
     |     i*i*i < i+i
     |   }
     |   if b then boundary.break(-i)
     |   -i-1
     | }
val res2: Int = 2

In the middle of computing the truth-value of b, we hit the if-statement that tells us that we actually know the answer to the whole thing, and we use the now-named context label from the outer context (outer) explicitly as the target ((using outer)).

It’s quite powerful! Strictly better than local and nonlocal returns. And at least in RC2, the optimization works just the way it should. And, unlike returns, you can abstract out the jumping capability so you can build functionality on top of it. This compiles, for instance:

  inline def jumpme(i: Int)(using lb: boundary.Label[Int]): Unit =
    if i > 99 then boundary.break(i)

  def multijump(i: Int): Int =
    if i < 0 then 0
    else boundary {
      if i > 10 then boundary.break(i)
      jumpme(i)
      Iterator.iterate(i)(_ * 2 + 1).
        takeWhile(_ < 10).
        foreach{ x =>
          jumpme(x)
          if (x % 3) == 0 then boundary.break(x)
        }
      i*i
    }

This allows us to abstract out the capability to terminate execution flow early! And it will be compiled to a jump, if the context is local.

Pretty awesome!

I don’t see any reason that one would want to use non-local returns at this point. (Except as it stands right now, you have to avoid Try and all other safe-handling code because the control flow is a regular stackless exception, not ControlThrowable. But the functionality in Try is not very hard to rewrite.)

So I think the answer to non-local returns is: wait for Scala 3.3.

2 Likes