Scala NonLocalReturns ergonomics are problematic

Trying to get code to compile on Scala 3.2.x without warnings, I tried using the suggested NonLocalReturns where I found it’s appropriate. I found that the ergonomics of it can easy lead to a missing given error that forces the user to apply specific type arguments.
See example:

import scala.util.control.NonLocalReturns.*
trait Animal
object Dog extends Animal
object Cat extends Animal

def animal(arg : Int) : Animal = 
  if (arg < 0) return Dog
  Cat

def animalNoLocalRet(arg : Int) : Animal = returning{
  if (arg < 0) throwReturn(Dog) //error
  Cat
}

def animalNoLocalRetFixed(arg : Int) : Animal = returning[Animal]{
  if (arg < 0) throwReturn[Animal](Dog) 
  Cat
}

Maybe it’s possible to create an alternative API that will not yield this manual tinkering with types, but if not, I suggest we update the documentation properly to reflect this (common) use-case.

If we change the definition of throwReturn to take a using ReturnThrowable[? >: T] instead of a ReturnThrowable[T], it seems like your ideal animalNoLocalRet compiles.

Would you like to submit a PR, with appropriate tests in tests/run/nonlocal-return.scala?

You could separate the ReturnThrowable from the “Returner”, and make the Returner contravariant.

object NonLocalReturns {
  case class ReturnThrowable[T](result: T, returner: Returner[T]) extends ControlThrowable
  class Returner[-T] {
    def throwReturn(result: T): Nothing = {
      throw ReturnThrowable(result, this)
    }
  }

  /** Performs a nonlocal return by throwing an exception. */
  def throwReturn[T](result: T)(using returner: Returner[T]): Nothing =
    returner.throwReturn(result)

  /** Enable nonlocal returns in `op`. */
  def returning[T](op: Returner[T] ?=> T): T = {
    val returner = new Returner[T]
    try op(using returner)
    catch {
      case ex: ReturnThrowable[T] =>
       if (ex.returner.eq(returner)) ex.result else throw ex
    }
  }
}

I can submit a PR. Is this considered a binary compatible change?
Is @Jasper-M’s proposal a better fit?

Yes, my suggestion is binary compatible. And TASTy compatible.

@Jasper-M 's proposal is perhaps cleaner, but definitely not binary compatible.

1 Like