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.