rfcs icon indicating copy to clipboard operation
rfcs copied to clipboard

async-await RFC

Open renatoathaydes opened this issue 6 years ago • 12 comments

This proposal adds a async/await syntax sugar to Pony in order to make it easier to write asynchronous code via Promises.

renatoathaydes avatar Mar 02 '19 21:03 renatoathaydes

Perhaps Im not finding it. What happens to await as a keyword if called in a function that isn't marked as async? Is that a syntax error?

SeanTAllen avatar Mar 02 '19 21:03 SeanTAllen

The to in translated code (that you say is sugar) still needs to be desugared:

async fun lambdaV(t: T): Promise[V] => ...
async fun lambdaW(v: V): Promise[W] => ...

fun async_fun(): Promise[W] =>
    let promise: Promise[T] = /* obtained from an async call */
    let result: Promise[W] =
        promise.next[V]({(t: T) => lambdaV(t)})
            .next[W]({(v: V) => lambdaW(v)})
    result

An important part of this is "what does async fun lambdaW(v: V): Promise[W]" get desugared to. That isn't clear to me.

SeanTAllen avatar Mar 02 '19 21:03 SeanTAllen

The to in translated code (that you say is sugar) still needs to be desugared:

async fun lambdaV(t: T): Promise[V] => ...
async fun lambdaW(v: V): Promise[W] => ...

fun async_fun(): Promise[W] =>
    let promise: Promise[T] = /* obtained from an async call */
    let result: Promise[W] =
        promise.next[V]({(t: T) => lambdaV(t)})
            .next[W]({(v: V) => lambdaW(v)})
    result

An important part of this is "what does async fun lambdaW(v: V): Promise[W]" get desugared to. That isn't clear to me.

In that case, those functions do not need to be async, they just need to return a Promise. Their contents would be like the one shown - but with an actor actually getting involved in fulfilling the promise - I left that out because that's already how it works today.

renatoathaydes avatar Mar 02 '19 21:03 renatoathaydes

I think it would help make the case for this RFC if you showed in motivation, something that is a pain to do with the existing Promises infrastructure compared to how it would look with the await idea.

SeanTAllen avatar Mar 02 '19 21:03 SeanTAllen

I think it would help make the case for this RFC if you showed in motivation, something that is a pain to do with the existing Promises infrastructure compared to how it would look with the await idea.

I have exactly that on my editor right now :) Will try to re-write it using the proposed syntax and post it here.

renatoathaydes avatar Mar 02 '19 21:03 renatoathaydes

As of right now, the lambdas that you give in the Promise::next method have to be sendable. You already covered how to handle captures. This however breaks the linearity of the written code: a variable that looks to be usable in a part of the function body would actually not be usable. It would be, in some way, "blocked by the await gatekeeper".

Which is why I am asking you, couldn't the await keyword have a body, in which the lexical scope is different from outside? Would this vary too much from your conception of what the async/await syntax should be doing/look like?

Instead of doing:

async fun foo(env: Env) =>
  env.out.print("Hey, I'm the env!")
  let meal = await(env) Waiter.serve() // Promise[String]
  env.out.print("Now let's eat our " + meal)

It would look like this:

async fun foo(env: Env) =>
  env.out.print("Hey, I'm the env!")
  await(env) meal from Waiter.serve() then
    env.out.print("Now let's eat our " + meal)
  else
    // on Promise failure
  end

Both would desuggar to this:

fun foo(env: Env) =>
  env.out.print("Hey, I'm the env!")
  Waiter.serve().next[None]({(meal: String)(env) =>
    env.out.print("Now let's eat our " + meal)
  }, {()(env) =>
    // on Promise failure
  })

Which brings me to the next point: how are promise rejections handled? Should the user be able to handle the error by itself, or will it always propagate the error up the promise chain? If the user may handle such errors, how could they do it?

adri326 avatar Mar 02 '19 22:03 adri326

Following along with @adri326. If await is a block (which makes sense) then it would need to have semantics similar to recover in that, in it, you can only access sendable variables from the outer scope.

SeanTAllen avatar Mar 02 '19 22:03 SeanTAllen

As far as I can tell, this is useful for "I am using Promises to do some processing as an actor unto themselves" rather than. I am sending a promise to another actor and it can use the promise to communicate a value back to me. If that is not the case @renatoathaydes, I think it would be wise to show how it can be used in the case of other usages of Promises.

If it is the case, that should be noted in the RFC.

SeanTAllen avatar Mar 02 '19 22:03 SeanTAllen

I like @adri326's proposal very much. It is more consistent with the language and at the same time more powerful as it allows handling a rejected promise (which is normally done by throwing an exception at the point of the await call,which simulates what would happen in a synchronus method - but that option is not available in Pony unless we made all await behave like partial functions, which would not be desirable, I think). Should I rewrite the RFC to take the better approach into consideration or the procedure is to create another RFC entirely?

renatoathaydes avatar Mar 03 '19 09:03 renatoathaydes

Just would like to ask, wouldn't the new syntax be more consistent with existing constructs if it looked like this?

async fun foo(env: Env) =>
  env.out.print("Hey, I'm the env!")
  await meal = Waiter.serve()(env) then
    env.out.print("Now let's eat our " + meal)
  else
    // on Promise failure
  end

I.e. the captured variables go in the second list of parameters of the call (like in a lambda today) returning a Future, and instead of await meal from ..., await meal = ..., which avoids using yet another keyword and maintains = for all assignments.

renatoathaydes avatar Mar 03 '19 09:03 renatoathaydes

Just would like to ask, wouldn't the new syntax be more consistent with existing constructs if it looked like this?

async fun foo(env: Env) =>
  env.out.print("Hey, I'm the env!")
  await meal = Waiter.serve()(env) then
    env.out.print("Now let's eat our " + meal)
  else
    // on Promise failure
  end

I.e. the captured variables go in the second list of parameters of the call (like in a lambda today) returning a Future, and instead of await meal from ..., await meal = ..., which avoids using yet another keyword and maintains = for all assignments.

The await ... (env) then would not work, because the expression could be interpreted as "call Waiter.serve(), then call apply() on the return value"

adri326 avatar Mar 03 '19 17:03 adri326

From sync:

Adding async/await to Pony is a lot of work that would require some type system changes as well as rewriting a large part of the compiler to support CPS.

That said, our interpretation of this RFC was it's really about making promises easier to use for fork/join type workloads. This we believe can be made easier via a couple means:

  1. Pony Pattern(s) that cover different ways to do fork/join type workloads using Pony (both with and without Promises)
  2. development of a library to make doing fork/join work distribution easier

If anyone is interested taking on either and needs help, I volunteered to help folks, so please reach out to me on Slack or via email and I can help you with that work.

SeanTAllen avatar Mar 07 '19 18:03 SeanTAllen