-
Notifications
You must be signed in to change notification settings - Fork 508
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Polymorphic async/await implementation #1924
Changes from 14 commits
dd7aa87
5490c06
47883bd
d6b9f99
ec051cd
9110831
3d92776
41d00ed
cea18d8
e544cb9
48fa432
e73d8ed
104a27c
ed3c88a
8971556
0220ebf
259b723
133094b
7cf2ad5
a2a9cb7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,189 @@ | ||||||||||||
/* | ||||||||||||
* Copyright 2020-2021 Typelevel | ||||||||||||
* | ||||||||||||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||||||||||||
* you may not use this file except in compliance with the License. | ||||||||||||
* You may obtain a copy of the License at | ||||||||||||
* | ||||||||||||
* http://www.apache.org/licenses/LICENSE-2.0 | ||||||||||||
* | ||||||||||||
* Unless required by applicable law or agreed to in writing, software | ||||||||||||
* distributed under the License is distributed on an "AS IS" BASIS, | ||||||||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||||||||
* See the License for the specific language governing permissions and | ||||||||||||
* limitations under the License. | ||||||||||||
*/ | ||||||||||||
|
||||||||||||
package cats.effect.std | ||||||||||||
|
||||||||||||
import scala.annotation.compileTimeOnly | ||||||||||||
import scala.reflect.macros.whitebox | ||||||||||||
|
||||||||||||
import cats.effect.kernel.Async | ||||||||||||
import cats.effect.kernel.syntax.all._ | ||||||||||||
import cats.syntax.all._ | ||||||||||||
import cats.effect.kernel.Outcome.Canceled | ||||||||||||
import cats.effect.kernel.Outcome.Errored | ||||||||||||
import cats.effect.kernel.Outcome.Succeeded | ||||||||||||
|
||||||||||||
class AsyncAwaitDsl[F[_]](implicit F: Async[F]) { | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should add some scaladoc being like "uh, this is really really unstable, only works on Scala 2.x, and will likely change and/or be deprecated in the future" There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Love it. |
||||||||||||
|
||||||||||||
/** | ||||||||||||
* Type member used by the macro expansion to recover what `F` is without typetags | ||||||||||||
*/ | ||||||||||||
type _AsyncContext[A] = F[A] | ||||||||||||
|
||||||||||||
/** | ||||||||||||
* Value member used by the macro expansion to recover the Async instance associated to the block. | ||||||||||||
*/ | ||||||||||||
implicit val _AsyncInstance: Async[F] = F | ||||||||||||
|
||||||||||||
/** | ||||||||||||
* Non-blocking await the on result of `awaitable`. This may only be used directly within an enclosing `async` block. | ||||||||||||
* | ||||||||||||
* Internally, this will register the remainder of the code in enclosing `async` block as a callback | ||||||||||||
* in the `onComplete` handler of `awaitable`, and will *not* block a thread. | ||||||||||||
*/ | ||||||||||||
@compileTimeOnly("[async] `await` must be enclosed in an `async` block") | ||||||||||||
def await[T](awaitable: F[T]): T = | ||||||||||||
??? // No implementation here, as calls to this are translated to `onComplete` by the macro. | ||||||||||||
|
||||||||||||
/** | ||||||||||||
* Run the block of code `body` asynchronously. `body` may contain calls to `await` when the results of | ||||||||||||
* a `Future` are needed; this is translated into non-blocking code. | ||||||||||||
*/ | ||||||||||||
def async[T](body: => T): F[T] = macro AsyncAwaitDsl.asyncImpl[F, T] | ||||||||||||
|
||||||||||||
} | ||||||||||||
|
||||||||||||
object AsyncAwaitDsl { | ||||||||||||
|
||||||||||||
type CallbackTarget[F[_]] = F[AnyRef] | ||||||||||||
type Callback[F[_]] = Either[Throwable, CallbackTarget[F]] => Unit | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this have to be public? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It does, because it's used in macro expansion, so the client code needs to have visibility over those types. These aliases have been really valuable during iteration as otherwise, any change to the types lead to having to amend the quasiquote expression in several places which is really, really annoying due to not getting compile errors until the macro expansion. I'd rather keep them for maintainability reasons but will obviously abide if you make an executive call to remove them. In the meantime, I removed There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm cool with keeping them. Just wanted to sanity check. |
||||||||||||
|
||||||||||||
// Outcome of an await block. Either a failed algebraic computation, | ||||||||||||
// or a successful value accompanied by a "summary" computation. | ||||||||||||
// | ||||||||||||
// Allows to short-circuit the async/await state machine when relevant | ||||||||||||
// (think OptionT.none) and track algebraic information that may otherwise | ||||||||||||
// get lost during Dispatcher#unsafeRun calls (WriterT/IorT logs). | ||||||||||||
type AwaitOutcome[F[_]] = Either[F[AnyRef], (F[Unit], AnyRef)] | ||||||||||||
djspiewak marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
|
||||||||||||
def asyncImpl[F[_], T]( | ||||||||||||
c: whitebox.Context | ||||||||||||
)(body: c.Tree): c.Tree = { | ||||||||||||
import c.universe._ | ||||||||||||
if (!c.compilerSettings.contains("-Xasync")) { | ||||||||||||
c.abort( | ||||||||||||
c.macroApplication.pos, | ||||||||||||
"The async requires the compiler option -Xasync (supported only by Scala 2.12.12+ / 2.13.3+)" | ||||||||||||
) | ||||||||||||
} else | ||||||||||||
try { | ||||||||||||
val awaitSym = typeOf[AsyncAwaitDsl[Any]].decl(TermName("await")) | ||||||||||||
def mark(t: DefDef): Tree = { | ||||||||||||
c.internal | ||||||||||||
.asInstanceOf[{ | ||||||||||||
def markForAsyncTransform( | ||||||||||||
owner: Symbol, | ||||||||||||
method: DefDef, | ||||||||||||
awaitSymbol: Symbol, | ||||||||||||
config: Map[String, AnyRef] | ||||||||||||
): DefDef | ||||||||||||
}] | ||||||||||||
.markForAsyncTransform( | ||||||||||||
c.internal.enclosingOwner, | ||||||||||||
t, | ||||||||||||
awaitSym, | ||||||||||||
Map.empty | ||||||||||||
) | ||||||||||||
} | ||||||||||||
val name = TypeName("stateMachine$async") | ||||||||||||
// format: off | ||||||||||||
q""" | ||||||||||||
final class $name(dispatcher: _root_.cats.effect.std.Dispatcher[${c.prefix}._AsyncContext], callback: _root_.cats.effect.std.AsyncAwaitDsl.Callback[${c.prefix}._AsyncContext]) extends _root_.cats.effect.std.AsyncAwaitStateMachine(dispatcher, callback) { | ||||||||||||
${mark(q"""override def apply(tr$$async: _root_.cats.effect.std.AsyncAwaitDsl.AwaitOutcome[${c.prefix}._AsyncContext]): _root_.scala.Unit = ${body}""")} | ||||||||||||
} | ||||||||||||
${c.prefix}._AsyncInstance.flatten { | ||||||||||||
_root_.cats.effect.std.Dispatcher[${c.prefix}._AsyncContext].use { dispatcher => | ||||||||||||
${c.prefix}._AsyncInstance.async_[${c.prefix}._AsyncContext[AnyRef]](cb => new $name(dispatcher, cb).start()) | ||||||||||||
} | ||||||||||||
}.asInstanceOf[${c.macroApplication.tpe}] | ||||||||||||
""" | ||||||||||||
} catch { | ||||||||||||
case e: ReflectiveOperationException => | ||||||||||||
c.abort( | ||||||||||||
c.macroApplication.pos, | ||||||||||||
"-Xasync is provided as a Scala compiler option, but the async macro is unable to call c.internal.markForAsyncTransform. " + e.getClass.getName + " " + e.getMessage | ||||||||||||
) | ||||||||||||
} | ||||||||||||
} | ||||||||||||
|
||||||||||||
} | ||||||||||||
|
||||||||||||
abstract class AsyncAwaitStateMachine[F[_]]( | ||||||||||||
dispatcher: Dispatcher[F], | ||||||||||||
callback: AsyncAwaitDsl.Callback[F] | ||||||||||||
)(implicit F: Async[F]) extends Function1[AsyncAwaitDsl.AwaitOutcome[F], Unit] { | ||||||||||||
|
||||||||||||
// FSM translated method | ||||||||||||
//def apply(v1: AsyncAwaitDsl.AwaitOutcome[F]): Unit = ??? | ||||||||||||
|
||||||||||||
// Resorting to mutation to track algebraic product effects (like WriterT), | ||||||||||||
// since the information they carry would otherwise get lost on every dispatch. | ||||||||||||
private[this] var summary : F[Unit] = F.unit | ||||||||||||
|
||||||||||||
private[this] var state$async: Int = 0 | ||||||||||||
|
||||||||||||
/** Retrieve the current value of the state variable */ | ||||||||||||
protected def state: Int = state$async | ||||||||||||
|
||||||||||||
/** Assign `i` to the state variable */ | ||||||||||||
protected def state_=(s: Int): Unit = state$async = s | ||||||||||||
|
||||||||||||
protected def completeFailure(t: Throwable): Unit = | ||||||||||||
callback(Left(t)) | ||||||||||||
|
||||||||||||
protected def completeSuccess(value: AnyRef): Unit = { | ||||||||||||
callback(Right(F.as(summary, value))) | ||||||||||||
} | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||||||||
|
||||||||||||
protected def onComplete(f: F[AnyRef]): Unit = { | ||||||||||||
dispatcher.unsafeRunAndForget { | ||||||||||||
// Resorting to mutation to extract the "happy path" value from the monadic context, | ||||||||||||
// as inspecting the Succeeded outcome using dispatcher is risky on algebraic sums, | ||||||||||||
// such as OptionT, EitherT, ... | ||||||||||||
var awaitedValue: Option[AnyRef] = None | ||||||||||||
(summary *> f).flatTap(r => F.delay{awaitedValue = Some(r)}).start.flatMap(_.join).flatMap { | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It actually surprises me that scalafmt gives this line a pass.
Suggested change
Also, why is the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I had disabled formatting before the quasiquotes and forgotten to re-enable it. Fixed in 8971556
2 birds, one stone :
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Is this correct : 0220ebf ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Almost! Commented on the commit There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I'm not sure I understand why the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
From the point of view of someone who doesn't know the innards of Dispatcher and how it relates to the effect's runtime, protecting against potential non-terminating behaviour seemed like a good reflex 😄. Also Dispatcher being polymorphic, can you guarantee that it'll be the case for all effect types ? (I reckon it'd be pretty bad if it wasn't the case, but still). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I think that any |
||||||||||||
case Canceled() => F.delay(this(Left(F.canceled.asInstanceOf[F[AnyRef]]))) | ||||||||||||
case Errored(e) => F.delay(this(Left(F.raiseError(e)))) | ||||||||||||
case Succeeded(awaitOutcome) => awaitedValue match { | ||||||||||||
case Some(v) => F.delay(this(Right(awaitOutcome.void -> v))) | ||||||||||||
case None => F.delay(this(Left(awaitOutcome))) | ||||||||||||
} | ||||||||||||
} | ||||||||||||
} | ||||||||||||
} | ||||||||||||
|
||||||||||||
protected def getCompleted(f: F[AnyRef]): AsyncAwaitDsl.AwaitOutcome[F] = { | ||||||||||||
val _ = f | ||||||||||||
null | ||||||||||||
} | ||||||||||||
|
||||||||||||
protected def tryGet(awaitOutcome: AsyncAwaitDsl.AwaitOutcome[F]): AnyRef = | ||||||||||||
awaitOutcome match { | ||||||||||||
case Right((newSummary, value)) => | ||||||||||||
summary = newSummary | ||||||||||||
value | ||||||||||||
case Left(monadicStop) => | ||||||||||||
callback(Right(monadicStop)) | ||||||||||||
this // sentinel value to indicate the dispatch loop should exit. | ||||||||||||
} | ||||||||||||
|
||||||||||||
def start(): Unit = { | ||||||||||||
// Required to kickstart the async state machine. | ||||||||||||
// `def apply` does not consult its argument when `state == 0`. | ||||||||||||
apply(null) | ||||||||||||
} | ||||||||||||
|
||||||||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor thing, but can we rename this file in lower-case to reflect the fact that it contains multiple top-level members? Like
asyncAwait.scala
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
8971556