scala/scala-async

Return statement

hordon opened this issue · 9 comments

It would be great to add return statement to async-await library because it simplifies the code.

Consider example with return and without return.

Login function with return statement:
def login(login: String, pass: String): Future[Status] = async {
    val passHashFuture = getPassFromDb(login)
    val passHashOption = await(passHashFuture)

    if (passHashOption.isEmpty) ret(false)

    val pass = passHashOption.get
    val passValid = validatePassword(pass, passHash)

    if (!passValid) ret(false)

    true
}
Login function without return statement:
def login(login: String, pass: String): Future[Status] = async {
    val passHashFuture = getPassFromDb(login)
    val passHashOption = await(passHashFuture)

    if (passHashOption.isDefined) {
        val pass = passHashOption.get
        val passValid = validatePassword(pass, passHash)

        if (passValid) {
            true
        } else {
            false
        }
    } else {
        false
    }
}

As you can see the first case looks much better then second one.

Not really. You shouldn't be using early returns in Scala code.
Your code is needlessly obfuscated and looks like Java code translated to Scala directly.
It should be:

def login(login: String, pass: String): Future[Status] = async {
    val passHashFuture = getPassFromDb(login)
    val passHashOption = await(passHashFuture)
    passHashOption.exists(validatePassword(pass, _))
}

Honestly, I've never seen a good use of early return in Scala other than being able to directly transform Java to Scala by some automatic translator. For validation we have require() / assert() and exceptions, for everything else - if you need to use early return, probably your method is too long / complex and you need to just split it.

I would like to see an early return capability as well

Case:
Imagine a business feature which requires a flow of a particular user experience through an app.
If this flow can be modeled as a set of precondition checks each returning different information to the user, I definitely prefer having it that way, instead of having a deeply nested if/else split.

Your first hit on any code style/refactor on how to make this code nicer is just that : early returns.
your function doesn't have to be super long to motivate this. Imagine a function on a single level of abstraction clearly explaining the overall business logic on about 5-10 lines. That is to me certainly preferable to 4x dispatching if/else functions where each branching level function is only perhaps 3 lines.

I've considered this, but the problem is that the code won't typecheck, as before the async macro is expanded, you'll hit a type error because you're returning, say, a String from a method with a return type of Future[String]. C# has special cased typechecking rules for return within an async method, but in Scala we've implemented async on top of the macro system, which doesn't let us do this.

@retronym I wouldn't mind having to write return Future(blah). In fact couldn't that just be forwarded through the earlyReturn-function?

To me, that sort of breaks the abstraction that the async {} block will wrap its result in a Future[T], and within that block you only need to produce a value of type T. I can see the argument that return is linked with the enclosing method, though, so it isn't a big surprise to return Future[T].

One design principle we've had with async is "when in doubt, don't deviate from the design in C#. For instance, we took the names of the await/async verbatim. This would be a deviation that I'd need to consider carefully.

Another surface syntax to consider would be providing a library alternative to return:

@compileTimeOnly def earlyReturn[T](t: T): T = ??? // or some other name
async {
   if (c) earlyReturn(01)
   await(1) + 2
}

The async macro could recognize this in the same way it recognizes await.

Maybe regular return and earlyReturn are actually distinctly useful features.

From "the peanut gallery", I would say that this makes sense

Early return can be implemented without typecheck easily with throwing exception.

object Async {

  def async[T](body: T)(implicit execContext: ExecutionContext): Future[T] = macro internal.ScalaConcurrentAsync.asyncImpl[T]

  @compileTimeOnly("")
  def await[T](awaitable: Future[T]): T = ???


  import scala.util.control.NoStackTrace
  case class EarlyReturnException[T](returnResult: T) extends Throwable with NoStackTrace

  def wrappedAsync[T](body: T)(implicit execContext: ExecutionContext): Future[T] = {
    val f = async(body)
    val p = Promise[T]

    f.onComplete {
      case Success(result) => p.success(result)
      case Failure(e) => e match {
        case retEx: EarlyReturnException[T] => p.success(retEx.returnResult)
        case _ => p.failure(e)
      }
    }

    p.future
  }

  def earlyReturn[T](value: T): Nothing = throw EarlyReturnException(value)
}

And usage:

wrappedAsync {
   if (c) earlyReturn(01)
   await(1) + 2
}

Marking this as out of scope.