Is there an async/await, bs-let, let_ppx official solution?

Totally agree on the macro sentiment. I wonder sometimes if ReScript would be suitable to have its own macro system on a language level. That would guarantee performance and syntax stability as opposed to the mess that writing a ppx is.

I do realise of course, that this would add a huge bag of complexity onto the parser (a debate would need to be had if that is an appropriate thing to do) as well as widen the gap towards Reason.

Still though… Some simple template language like https://github.com/jaredly/reason-macros shouldn’t break the bank. And again: Yes - I realise that achieving the beauty and comfort of the graphql-ppx would be next to impossible with such a simplistic concept.

1 Like

A risque take is that it might make sense to integrate use-cases that most people need a PPX for into the language. For instance GraphQL. ~40% of people that install ReScript also install graphql-ppx so having native GraphQL primitives in the language might also be an interesting direction.

3 Likes

We have this now–the ReScript compiler :slight_smile:

But being serious for a second, the ReScript team has long recommended checking in generated JS sources. For applications (as opposed to libraries), this is absolutely the right move. I believe the compiler skips re-compiling code that already has up-to-date JS output. So build times should not really be a concern even with PPX, if generated JS is checked in.

1 Like

Haven’t worked with GraphQL for a while, but a couple of years ago separate .graphql files meant way better overall DX, especially in WebStorm. So maybe in this particular case, I’d choose separate files over the ppx anyway.

That said… styled-ppx looks attractive. Yes, Tailwind looks attractive too, but maybe that’s too drastic a change. We have a lot of Emotion-based styles here already, and besides, css-in-res has an upside of having one compiler for everything.

1 Like

Sorry to revive this thread, but I recently ran into bs-let not working with ReScript and I was wondering what others do when they’d like to do something like async/await.

1 Like

I am just using plain promises with rescript-promise. bs-let is problematic in many different ways, and we don’t recommend extending the syntax for upwards compatibility and code robustness reasons.

1 Like

Just curious, in what different ways are bs-let problematic?

1 Like

apparently this is the right timing to ask for this :slight_smile:

3 Likes

Got it, okay. I hadn’t seen your library. I’m just starting a new project and was using GitHub - aantron/promise: Light and type-safe binding to JS promises because it seemed popular. I like that your library can interact with plain promises though. Do you have any thoughts about that library in contrast with your own? It would be nice to not have to translate native promises all over my code base.

I tried to explain the rationale of rescript-promise, with a comparison to aantron/promise (aka reason-promise in the PROPOSAL.md document.

Both can interact with Js.Promise.t types, but rescript-promise doesn’t add any extra apis to differentiate between rejectable and unrejectable promises. aantron/promise is a small wrapper around promises and allows rejection tracking of promises within your app. It also fixes an edge case where nested promises cause wrongly reported types in the type system (which we consider rare in normal use).

This has ups- and downsides in ergonomics though, especially if you are doing a lot of interop. The rescript-promise apis are also more familiar for JS devs, and will later be part of the core bindings that are shipped with the compiler.

To summarize: If you can deal with the extra complexity, and wish to track every single rejectable promise in your codebase, you can try aantron/promise. For the light version “that just works like in JS” with a little less guarantees, use rescript-promise.

2 Likes

Thank you for the reply! I didn’t realize you compared them in your proposal.

@ryyppy this never got a reply. In what ways is bs-let problematic?

@zth

bs-let works. But in rescript syntax the ppx syntax nests the calls anyway (so no use in using it).

For example (probably not the best example, but from a random file in my project):
Reason Syntax

        let%AwaitThen _ =
          HelperActions.mintDirect(
            ~marketIndex,
            ~amount=initialAmountShort,
            ~token=paymentToken,
            ~user=testUser,
            ~longShort,
            ~oracleManagerMock=oracleManager,
            ~isLong=false,
          );
        let pricesBelow = Belt.Array.makeBy(numberOfItems - 1, i => i);

        let%AwaitThen (_, resultsBelow) =
          pricesBelow->Array.reduce(
            (initialPrice, [||])->JsPromise.resolve,
            (lastPromise, _) => {
              let%AwaitThen (lastPrice, results) = lastPromise;
              let newPrice = lastPrice->sub(CONSTANTS.tenToThe18);
              let%AwaitThen _ =
                oracleManager->OracleManagerMock.setPrice(~newPrice);

              let%AwaitThen _ =
                longShort->LongShort.updateSystemState(~marketIndex);
              let%AwaitThen shortValue =
                longShort->LongShort.syntheticTokenPoolValue(
                  marketIndex,
                  false/*short*/,
                );
              let%AwaitThen longValue =
                longShort->LongShort.syntheticTokenPoolValue(
                  marketIndex,
                  true/*long*/,
                );

              (
                newPrice,
                [|
                  (
                    newPrice->Ethers.Utils.formatEther,
                    shortValue->Ethers.Utils.formatEther,
                    longValue->Ethers.Utils.formatEther,
                  ),
                |]
                ->Array.concat(results),
              )
              ->JsPromise.resolve;
            },
          );
        prices :=
          prices.contents
          ->Array.concat(resultsBelow)
          ->Array.concat([|
              (
                initialPrice->Ethers.Utils.formatEther,
                initialAmountShort->Ethers.Utils.formatEther,
                initialAmountLong->Ethers.Utils.formatEther,
              ),
            |]);
        ()->JsPromise.resolve;

Turns into the following rescript:

      %AwaitThen({
        let _ = HelperActions.mintDirect(
          ~marketIndex,
          ~amount=initialAmountShort,
          ~token=paymentToken,
          ~user=testUser,
          ~longShort,
          ~oracleManagerMock=oracleManager,
          ~isLong=false,
        )
        let pricesBelow = Belt.Array.makeBy(numberOfItems - 1, i => i)

        %AwaitThen({
          let (_, resultsBelow) = pricesBelow->Array.reduce((initialPrice, [])->JsPromise.resolve, (
            lastPromise,
            _,
          ) =>
            %AwaitThen({
              let (lastPrice, results) = lastPromise
              let newPrice = lastPrice->sub(CONSTANTS.tenToThe18)
              %AwaitThen({
                let _ = oracleManager->OracleManagerMock.setPrice(~newPrice)

                %AwaitThen({
                  let _ = longShort->LongShort.updateSystemState(~marketIndex)
                  %AwaitThen({
                    let shortValue =
                      longShort->LongShort.syntheticTokenPoolValue(marketIndex, false /* short */)
                    %AwaitThen({
                      let longValue =
                        longShort->LongShort.syntheticTokenPoolValue(marketIndex, true /* long */)

                      (
                        newPrice,
                        [
                          (
                            newPrice->Ethers.Utils.formatEther,
                            shortValue->Ethers.Utils.formatEther,
                            longValue->Ethers.Utils.formatEther,
                          ),
                        ]->Array.concat(results),
                      )->JsPromise.resolve
                    })
                  })
                })
              })
            })
          )
          prices :=
            prices.contents
            ->Array.concat(resultsBelow)
            ->Array.concat([
              (
                initialPrice->Ethers.Utils.formatEther,
                initialAmountShort->Ethers.Utils.formatEther,
                initialAmountLong->Ethers.Utils.formatEther,
              ),
            ])
          ()->JsPromise.resolve
        })
      })

@JasoonS thank you for the clarification!

However, I was mainly curious why using it is problematic in itself, as @ryyppy was saying. So, @ryyppy any chance for a clarification?

1 Like

Are you asking why we were undecided on the general idea of monadic-let transformations?

If so, I will just dump my (of course highly subjective) perspective on this whole thing.

So my personal nitpick on the latest “stage-0” proposal (the one where Bob essentially proposed a somewhat specialized version of bs-let) was the fact that it felt like a bolted-on solution for a very visible, and very high-impact addition to the language that was just asking to be abused. The proposal document itself was already hard to understand, and from a design perspective, the feature was not specifically locked to promises even though it was only supposed to be used with promises (and node callbacks). For a few folks (espcially those churn resistent OCaml users) it was a “good enough” solution, but from a language ecosystem perspective it was a risky addition.

My fear was also backed by some anecdotical data: The new editor support was forked from the original RLS codebase, which was full of monadic let usages for all kinds of data structures. This particular syntax turned out to be a pretty big source for swallowed errors, causing a lot of annoying hard to find bugs that directly translated into unhappy IDE users (I hope we all agree that the old vscode extension aged badly over time). The stability of the code improved greatly as soon as Cheng / Christiano got rid of all the hard to understand let%xyz to make uncovered error cases more explicit and hard crashes less likely. This particular scenario seriously scared me; just thinking about how many ppl could have used that feature without noticing critical bugs, and how much impact that would have on the general quality of the ecosystem.

Of course this is only one data point, and I can’t wait to hear all the counter arguments on “how we did things wrong”, “how it’s not that bad” or “how I don’t understand the topic / let% syntax”. If experienced users have such a hard time making it behave correctly (including the creator of RLS / bs-let himself), then that’s an indicator that there is something wrong with the design. From a product management perspective, I’d rather postpone and re-evaluate the async / await situation in a later point in time instead of adding a somewhat working feature that needs to be maintained forever.

Previously I thought Bob / Cheng had some ideas regarding a coroutine like runtime library, and proper syntactic support along with it, but unfortunately this was apparently a misunderstanding and was never supposed to happen. This kinda took me by surprise as well, so all I can say is let’s wait it out and see what will happen in the future.

The async / await syntax is also not the only feature we had to put on hold for later. We once had a great proposal for a cleaner external syntax, which Maxim put a ton of time into (it was an incredibly well specified proposal + implementation). Even though it was solid, It didn’t make it because there was no proper consent. I am fine with it though, because I know that there is thought and discussion put into changes to the language, and we can still pick it up later when there is time to improve it.

As a last remark I also wanted to say that in my professional time, I am a ReScript user myself, so all this decisions being made impact me as much as anyone else here. I’d hate to see the language being bloated with things that seem like a good idea today, but then bite us years later (we had enough of those features in the past, and now we are struggling getting rid of them).

9 Likes

My only 2 cents on that is rescript has already achieved such a level of maturity, efficiency and feature completeness (including for DX) that the lack of support for async await is quite visible as being the last thing lacking, I can’t see any feature more important than this now for the ecosystem, so I really hope that this failed try (and that’s OK, we also want robust and elegant solutions and those are unlikely to be found at first) won’t focus the team away from this issue.

9 Likes

Yes, that’s the type of clarification I was after, thanks.

So, the basic idea is that it can “swallow” errors in a less obvious way than for example manually using a catch all with an ignore in a switch, and that’s dangerous enough in itself that you don’t want to support it. Have I understood it correctly?

Has there been talk of adding algebraic effects to the language to support async/await?

I’m sure there are many technical challenges to actually doing it, but does it fit within the design goals of the language?

I’ve spun this out into a separate topic: Algebraic effects

2 Likes

Apropos: Beware of Async/Await | Brandon's Website :

the easiest and clearest way to write something with async/await is, more often than not, wrong. And wrong in a subtle way that may never be noticed, because it only affects performance, not correctness.

2 Likes

optional syntax sugar is more welcome

currently the Optional.map is the most code snippet occur in my codebase and it’s biggest issue for promotion rescript to my team