proposal-do-expressions icon indicating copy to clipboard operation
proposal-do-expressions copied to clipboard

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

Open mattiamanzati opened this issue 6 years ago • 10 comments

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)
}

mattiamanzati avatar Jan 18 '19 14:01 mattiamanzati

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.

Maxdamantus avatar Mar 06 '19 08:03 Maxdamantus

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

munizart avatar Feb 03 '20 16:02 munizart

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)
}

entropitor avatar Sep 16 '21 07:09 entropitor

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 avatar Sep 16 '21 07:09 Shinigami92

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

entropitor avatar Sep 16 '21 08:09 entropitor

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.

Shinigami92 avatar Sep 16 '21 08:09 Shinigami92

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.

Maxdamantus avatar Sep 16 '21 09:09 Maxdamantus

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:

js-choi avatar Sep 16 '21 14:09 js-choi

@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 avatar Sep 16 '21 15:09 entropitor

@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

entropitor avatar Sep 16 '21 15:09 entropitor