A proposal for async style sugar

Hi all,
I made a draft of proposal for async style syntactic sugar, it is available here: Draft: Programmable let async for continue passing style in ReScript - Google Docs
Note this is in pretty early stages, similar to stage 1 in ES proposals, but technical discussions are welcome

20 Likes

Wheee. Some movement around promise nesting :drooling_face: Much appreciate this. I think I misunderstand something from the draft, so may I ask a few questions?

So, here’s an example from the RFC:

let async (err,data) = readFile('etc/passwd1')
if (err == null) raise err
let async (err,data2) = readFile('etc/passwd2')
if (err == null) raise err
body

I’ll slightly transform it:

let readPasswords = () => {
  let async (err,data) = readFile('etc/passwd1')
  if (err == null) raise err
  let async (err,data2) = readFile('etc/passwd2')
  if (err == null) raise err
  (data, data2) // <- result
}

Albeit this should compile fine (?) I find it is counter-intuitive that the result type of readPasswords is unit and not (string, string) tuple. Or will it fail to compile with “Somewhere unit is expected, got (string, string)”? Better, but not very intuitive still.

And… what if the “async” Node-style function will fire callback twice or 10 times? The code which looks sequential will be executed “randomly” at “random” moments. Is it acceptable?

I mean, in its current state, the idea of using let async for Node-style callbacks is compelling and elegant but wouldn’t it hurt more than help in such not-so-edge cases? In my experience, the vast majority of Node functionality is available in the form of promises for a long time already. So it might not worth the effort to bother with the callback-style calls at all.


About limitations. Here’s one listed:

Abused by trivial monads, e.g, option, list etc

Am I understand correctly, the intention of the let async is to be just for things that “take time”? If so, what is the reason to draw a solid line between the promise monad and all others? In ReScript we have options and results as first-class citizens, why refusing to get them on the board too?

The mechanics is the same and is happily exploited by happy folks in F# (let!), Haskell/Purescript (do), ReasonML (letop, bs-let). JavaScript is missing them because they have nulls instead of strong monads. And to solve the problem, at the very shallow, they got ?? and .?. I think ReScript might be easily superior than that.

Perhaps, if the let async is locked to Promises, people will invent trivial patterns to convert their Options, Results, etc to Promises only to use the let async and avoid deep nesting. But this should look silly.

let async a = aOption->Foo.Option.promisify
let async b = bResult->Foo.Result.promisify
makeStuff(a, b)
12 Likes

Am I understand correctly, the intention of the let async is to be just for things that “take time”?

Yes, mostly for callback in the event loop.

If so, what is the reason to draw a solid line between the promise monad and all others? In ReScript we have option s and result s as first-class citizens, why refusing to get them on the board too?

This style is in-efficient for trivial monads, it created nested closures which is mostly performance killer.

The scope is unclear so that readability from the semantics point of view is not that good.

1 Like

I agree it’s a bit of an overhead with options, but it can be quite nice with result types. Maybe with some optimization the closures can be inlined? (or is that technically challenging?)

I like the proposal. It looks like it’s basically monadic let operators with a slightly different syntax (or am I wrong?).

1 Like

Maybe with some optimization the closures can be inlined?

ReScript is already doing the best in the market. The best optimization is actually to do the less things in the beginning instead of relying the magic of compilers.

2 Likes

This can be a big gotcha for the callback style, if you come at this from the await feature in JS, it could be a footgun.

I think the biggest reason is to ensure that this can be compiled to async await in the JS output, which can only be done with Promise. The engines already optimize async await by having less event loop ticks which makes them more performant (see v8.dev/blog/fast-async.

The resulting JS is also more readable, which Rescript cares about.


In my opinion, the callback version should be ignored for now and focus in making the Promises version as nice and similar to Js as possible, including the error messages. Sounds like a lot of work already.

This way there is also no hidden then function used, since the standard library Promise then would be used.

I’d do something straight from JS if possible, would help adoption in my opinion:

let fn = async (a, b) => {
  let x = await e1
  x
}

Could become for now:

let fn = (a, b) => {
  e1->Promise.then(x => {
    x
  })
}

And it would map very directly to modern JS async/await in the future when there is time to address that.

5 Likes

There is also the interactions with try/catch and promises to take into account, given what happens in JS.

If that is not going to be tackled it may be better to have a different syntax like the proposed let async to not confuse users thinking that async/await could behave like JS’s async/await.

4 Likes

Eagerly awaiting this or a refinement of it. Been the one major “bugbear” having come from a js background. Promise chaining is awkward, and while typesafe, feels a bit gross in a lot of use cases. Think this could be the last frontier for rescript to really take over as the premium js alternative.

3 Likes

Hi, just to put my 2 cents - because this change is still important to me and I’m waiting on more news about it eagerly.

Currently we have to use the old reasonml syntax with the bs-let ppx. We are using reason for all our tests and backend, and it is hard to find a function call anywhere in that code that isn’t a promise - so just using promise chaining with then is completely unreadable and hard to maintain.

I saw that this feature wasn’t included in the roadmap. This is my big blocker for working with rescript in those promise heavy code-bases.

As for the syntax, I think it looks great. I’m less concerned about the whole thing being generalizable. Something simple, that works is great for me - no need for it to look exactly like javascripts async/await etc etc.

Incase it is useful, these are the 2 let ops I have defined (for the map/flatMap equivalents of a promise) in reason syntax:

type t(+'a) = Js.Promise.t('a);

[@send] external then_: (t('a), [@uncurry] ('a => t('b))) => t('b) = "then";

[@send]
external map: (t('a), [@uncurry] ('a => 'b)) => t('b) = "then";

module Then = {
  let let_ = then_;
};
module Await = {
  let let_ = map;
};

I often just end up using the Then but it is nice to have the Await for the last promise in the nesting.

As for my opinion on the performance hit of abusing this syntax, I think for most people it is negligable, but maybe a warning flag that can be disabled explaining that nesting promise scopes isn’t good for performance is a good idea.

Another option/question: could the bs-let ppx be made compatible with rescript syntax in the interim?

5 Likes