proposal-do-expressions
proposal-do-expressions copied to clipboard
do as computation-expression to solve "async" do and "generator" do
Not sure, but it would be interesting to support instead of just "do" something like computation-expressions in FSharp (see doc here https://docs.microsoft.com/dotnet/fsharp/language-reference/computation-expressions)
This would allow async do's as exposed in computation-expression. This syntax is more lower-level than the actual do we are proposing here, and the "do" proposed here is just an implementation of a computation expression
Some examples are present in the link above.
With promises:
const accessToken = "abcd"
const currentUserPromise = async {
let! userId = fetchCurrentUserIdByToken(accessToken)
return! fetchUserById(userId)
}
Just to add further commentary: personally, I think this would have been better than introducing the concept of "asynchronous functions" (and likewise, generator functions), since there was really no need to conflate async/await with functions.
What we have:
const foo = async (foo, bar) => {
return await foo + await bar;
};
What we could have had instead (and could still have as an addition, given this suggestion):
const foo = (foo, bar) => async {
return await foo + await bar;
};
Admittedly, the equivalent non-arrow function isn't as concise, but I think having async/await as a feature independent of functions would have been overall simpler.
For prior art on using Do for monadic things (like Promises and arrays): https://github.com/purescript/documentation/blob/master/language/Syntax.md#do-notation
I agree computation expressions would be even better than async/await as it can not just be used for promises. I do think that the let!
from fsharp is not ideal though. I like how in JS the await is in the logical place but I guess it could be replaced with a different keyword or symbol like below:
const currentUserPromise = async {
const userId <- fetchCurrentUserIdByToken(accessToken)
return <- fetchUserById(userId)
}
I personally would vote against return!
cause return! fetchUserById(userId)
can be very quickly be confused with return !fetchUserById(userId)
. And this fulfills a complete different meaning!
@entropitor The usage of <
(or >
) is potentially dangerous in usage with jsx/tsx :slightly_frowning_face:
@Shinigami92 It was just a proposal, the actual symbol / keyword could be defined later, I don't think it makes a huge difference. I'd probably even use it when the symbol was <-------------------------- it's that useful 😁
But I do think having the keyword / symbol in the same place as the await keyword will help with understanding the code as the symbol is in the place where you need it to understand the flow, unlike the let!
and return!
Yeah :slightly_smiling_face:
I think the discussion about the explicit return
is already here: https://github.com/tc39/proposal-do-expressions/issues/55
I start to like the return.do
variant. But hadn't thought about it to much yet.
I'm assuming @entropitor meant to write:
const userId = <-fetchCurrentUserIdByToken(accessToken)
rather than:
const userId <- fetchCurrentUserIdByToken(accessToken)
But yes, I think the way it appears syntactically like a unary operator is preferable in a language like JS, which is already an imperative language, in contrast to pure (or at least more functional) languages. I think <-
in particular is reasonable, and interestingly it corresponds with the operator in Go for receiving a value from a channel.
To elaborate on the reason for the different notation in "pure" languages such as PureScript and Haskell: in these languages, evaluation order is at least meant to be implicit. Given a Haskell expression such as:
let a = g x in
let b = h y in
f a b
you don't know which of g
or h
will be invoked first (it's even possible that neither will be invoked), because semantically, f
will receive something like a thunk for each argument, which is only evaluated when needed (this is why Haskell is described as "call-by-need" rather than "call-by-value" as in JavaScript). Accordingly, it wouldn't really make sense to have something like the await
construction in Haskell:
-- NOTE: this is intentionally nonsensical
do {
let a = await (g x) in
let b = await (h y) in
resolve (f a b)
}
To avoid breaking existing expectations around lack of evaluation order, they introduced a separate syntax, which can be seen as implying an actual order:
do {
a <- g x;
b <- h y;
resolve (f a b)
}
As far as I know, PureScript and F# are both call-by-value rather than call-by-need, so you can technically probably assume an evaluation order in an expression like the one above (or equivalently, f (g x) (h y)
), but you're meant to avoid making those assumptions, as they imply that your code is impure.
For what it’s worth, I’m exploring adding F# computation expressions as a future proposal. I’ve been writing in a Gist about this idea: ES “context blocks”. (Warning: It’s very early days and it will probably be mid-2022 at the earliest before I’ll have this in a presentable shape. And a lot of old writing is hidden inside a “Scratchpad” disclosure element at the end.)
Excellent reading for those who haven’t yet seen it:
@Maxdamantus I was thinking that <-
could potentially also be short for = <-
but honestly that really doesn't matter at all.
And I get your point about Haskell but in do notation the order matters very much because we are not actually assigning variables but constructing lambda's behind the scenes:
do {
a <- g x;
b <- h y;
resolve (f a b)
}
=== bind (g x) (\a -> bind (h y) (\b -> resolve (f a b)))
and if e.g. (g x) returns None
then \a -> ...
never even gets called let alone h y
@js-choi That's looking really interesting. I'm looking forward to a full proposal as I think it's one of the few missing pieces to let FP truly shine in JS/TS