Proposal: then operator for easier promise manipulation
Why not just use async/await then? Seems like if you were to replace then
with await
in your top example it would work exactly as you want (as long
as it's in an async function).
The purpose of await
is to allow synchronous execution within an
async function.
The purpose of then
is to make it easier to manipulate the promises
as if they were normal values. It is much closer to
Promise.prototype.then
than to await
, since it does not prevent
the statements following it from execution. For this reason it doesn't
need to be in an async function.
The synchronous nature of await
can be annoying when you have
multiple promise-creating expressions that you want to run in
parallel. You have to run the expressions first, keep track of the
promises they evaluate to, and then use await
on those promises.
Promise.all
helps in this endeavor, but it prevents you from putting
expressions exactly where you want them to be.
Consider the following:
const pets = (await getDogs()).concat(await getCats())
This would not be ideal if you want getDogs
and getCats
to start
off in parallel. So you need to do something like this:
const [dogs, cats] = await Promise.all([getDogs(), getCats()])
const pets = dogs.concat(cats)
With then
it could be done like so:
const pets = await (then getDogs()).concat(then getCats())
On Sat, Mar 3, 2018 at 1:04 AM, Bryant Petersen <petersen.ok at gmail.com> wrote:
The purpose of
await
is to allow synchronous execution within an async function.The purpose of
then
is to make it easier to manipulate the promises as if they were normal values. It is much closer toPromise.prototype.then
than toawait
, since it does not prevent the statements following it from execution. For this reason it doesn't need to be in an async function.The synchronous nature of
await
can be annoying when you have multiple promise-creating expressions that you want to run in parallel. You have to run the expressions first, keep track of the promises they evaluate to, and then useawait
on those promises.Promise.all
helps in this endeavor, but it prevents you from putting expressions exactly where you want them to be.Consider the following:
const pets = (await getDogs()).concat(await getCats())
This would not be ideal if you want
getDogs
andgetCats
to start off in parallel. So you need to do something like this:
const [dogs, cats] = await Promise.all([getDogs(), getCats()]) const pets = dogs.concat(cats)
With
then
it could be done like so:
const pets = await (then getDogs()).concat(then getCats())
I'm not sure how you're able to continue running code after the first
"then". The .concat
part is a property lookup on the first value,
which can run arbitrary code - what object does that code operate on?
How do you get to the point of even noticing that there's an argument
to the function that also needs to be then'd?
I think the only way to get this to work would be to do a pre-parse
over the function, looking for "then" expressions, and collect them
all together into an implicit "await Promise.all(...)", effectively
run at the beginning of the function when you execute it "for real".
(Kinda similar to var
hoisting, which implicitly puts a lot of var x = undefined;
statements at the top of your function.) How this
squares away with the possibility of a later "then" actually depending
on the value of an earlier "then" expression, or on a value outside of
the "then" expressions, is unclear.
Using Promise.all() to avoid synchronously waiting on each promise one-by-one is a little awkward sometimes, but it gives you obvious, well-defined semantics, which this doesn't appear to have.
An expression which contains a then
operator is not run
synchronously. It does not run until its then
expressions are
resolved. It is like the body of a callback function, since the
statements following it will most likely run before it does.
The way I would implement this is by having certain expressions search
their subexpressions (i.e. AST decendants) for then
expressions. If
it finds one, it converts itself to an immediately invoked async
function expression. This IIAFE's body will run the arguments to each
then
expression (so they're run in parallel), after that it will
return an expression very similar to its original form. The IIAFE
evaluates to a promise resolving to what you'd expect the original
expression to resolve to.
The only thing at this point I consider not to be well-defined is what "certain expressions" need to do this search and conversion. I call this out in my more detailed explanation of the proposal: bwpetersen/proposal-then-operator#cutoff-points. I think there are obvious heuristics for deciding which expressions should do this.
This operator doesn't make any sense to me. It has to know not only the immediate expression to its left and/or right like normal operators.
It's absolutely not a unary operator, as it has to have information about every outer operator. It's almost an inverted operator in that sense.
If we're saying that the precedence is equal to the await operator, then
what does await then x
result in? Given your examples, it should result
in something like x.then(x => await x)
Heck, how do you operate on the resulting promise without an intermediate variable? Can you even?
This is extremely unintuitive, as (then x)
should behave the same
regardless of where it appears. With your proposal, it's completely
inconsistent.
const y = something(then x);
y.then(x => log(x));
// is equivalent to
x.then(x => something(x)).then(x=> log(x))
// vs
(something(then x)).then(x => log(x));
// is equivalent to
x.then(x => something(x).then(x => log(x)));
It is crazy that this kind of ambiguity could even be considered. How do you determine how far up in the expression tree you go?
Plus, the usefulness of this is marginal at best since we already have async/await.
On Mar 3, 2018 21:00, "Bryant Petersen" <petersen.ok at gmail.com> wrote:
An expression which contains a then
operator is not run
synchronously. It does not run until its then
expressions are
resolved. It is like the body of a callback function, since the
statements following it will most likely run before it does.
The way I would implement this is by having certain expressions search
their subexpressions (i.e. AST decendants) for then
expressions. If
it finds one, it converts itself to an immediately invoked async
function expression. This IIAFE's body will run the arguments to each
then
expression (so they're run in parallel), after that it will
return an expression very similar to its original form. The IIAFE
evaluates to a promise resolving to what you'd expect the original
expression to resolve to.
The only thing at this point I consider not to be well-defined is what "certain expressions" need to do this search and conversion. I call this out in my more detailed explanation of the proposal: bwpetersen/proposal-then-operator#cutoff-points. I think there are obvious heuristics for deciding which expressions should do this.
Thank you for the questions.
How do you determine how far up in the expression tree you go?
I think this is a good question. When I go through various
expressions, it seems obvious to me given the intent of the operator
where to draw the line. In cases like let x = ...
, function() { ... }
, await ...
, and then ...
it shouldn't go higher than ...
.
Part of the motive for posting it here is to get some discussion about
this.
If we're saying that the precedence is equal to the await operator, then what does
await then x
result in? Given your examples, it should result in something likex.then(x => await x)
It shouldn't go higher than the ...
in await ...
, so it results in
something like await x.then(x => x)
.
Heck, how do you operate on the resulting promise without an intermediate variable? Can you even?
You get to operate on the promise as if it were any value. Would you
be specific about the operation you would like to do that you think
becomes inaccessible? My best guess is you would want to use
Promise.resolve
.
I've put the details here: bwpetersen/proposal-then-operator
The basic idea is that the
then
operator would allow you to manipulate a promise as if it were its resolved value. Expressions containingthen
become similar to anonFulfillment
handler.Here is an example:
const greeting = `Hello, ${(then getUser()).name}. You have ${(then getMessages()).length} messages.` // This would behave like: const greeting = async () => { const user = getUser() const messages = getMessages() return `Hello, ${(await user).name}. You have ${(await messages).length} messages.` }()
I'm looking for feedback on this idea.