tc39 / proposal-do-expressions

Proposal for `do` expressions
MIT License
1.11k stars 14 forks source link

do as computation-expression to solve "async" do and "generator" do #38

Open mattiamanzati opened 5 years ago

mattiamanzati commented 5 years ago

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)
}
Maxdamantus commented 5 years ago

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.

munizart commented 4 years ago

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

entropitor commented 2 years ago

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)
}
Shinigami92 commented 2 years ago

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:

entropitor commented 2 years ago

@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!

Shinigami92 commented 2 years ago

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.

Maxdamantus commented 2 years ago

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.

js-choi commented 2 years ago

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:

entropitor commented 2 years ago

@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

entropitor commented 2 years ago

@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