async-await RFC
This proposal adds a async/await syntax sugar to Pony in order to make it easier to write asynchronous code via Promises.
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?
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.
The
toin 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)}) resultAn 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.
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 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.
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?
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.
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.
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?
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.
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 endI.e. the captured variables go in the second list of parameters of the call (like in a lambda today) returning a
Future, and instead ofawait 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"
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:
- Pony Pattern(s) that cover different ways to do fork/join type workloads using Pony (both with and without Promises)
- 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.