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

So I’m kinda sold on the new ReScript syntax, I feel it’s great for those of us who have been developing in ReasonML and who are coming from JS land.
I found bs-let super handy when I had to build my promise-based (fetch-based) APIs. I couldn’t make it work on ReScript though.
So I just wanted to ask if there’s an official way to have that async/await sugar in ReScript, or do we need to wait on a newer OCaml release to have something like that available in ReScript?

2 Likes

Hey!

There is a statement about async / await on the documentation here:

ReScript’s primary mechanism for async programming is the same as JavaScript’s (callbacks and promises), since we compile cleanly to JavaScript and would like to avoid dragging in a heavy custom runtime.

However, it is planned for us to introduce a coroutine-like feature in the future; for that reason, we’re postponing introducing the keywords async and await into the language; though our (upcoming) Promise API bindings revamp + pipe will make your async code already look better than otherwise.

Hope this helps!

Edit: The cited text above is outdated, please refer to the up to date promise docs for the latest statement on async / await

3 Likes

Will PPXes be supported by ReScript?

ppxes are still expressable in ReScript, but they are not syntactically optimized iirc. The compiler itself is currently built in a way that makes PPX setups brittle, and they also cause taxing build time overhead when used heavily, especially when you mix different ppxes.

E.g some ppl couldn’t upgrade to the newest compiler bc they were relying on abandoned community ppxes, in some codebases we realized that ppxes take half the time of the build. Some ppl might argue that this is fine, that the advantages of said ppx is worth the build time trade off. Problem with that is that the whole ReScript toolchain heavily relies on the compiler’s build performance speed, so e.g. right now our Editor plugin requires the compiler builds to be in max 200ms range to be reliably responsive when users try to autocomplete, or try to get type hints. It is hard / almost possible to predict the compiler speeds when ppl start extending the syntax in various different ways. Of course we could mitigate build performance issues by caching build results etc, but this is a technical debt we want to push out to the future, until we really really need it.

That’s the reason why we generally advice to not use ppxes, but I do understand why in some use-cases it’s impossible to not use them.

4 Likes

What I’d personally like to see more are meta programming tools that do classical code generation (read in some config file / graphql file / json schema file, emit some ReScript module), than trying to embed their meta programming in the language itself.

This eliminates a lot of problems we have right now: the compiler wouldn’t be slowed down by running external binaries all the time, and the emitted source would be easy to inspect.

For the typical syntactical use cases like async / await and optional access, we will get dedicated syntax at some point, which will heavily improve readibility. It would also allow us to standardize our community on one syntax, instead of telling everyone to use ppx1 for x and ppx2 for y.

2 Likes

That means having a completely separate watcher + build system just for GraphQL and handle all the orchestration of that. And now you are not able to declare queries in your ReScript code files.

The current state of PPX is better than the alternatives for practical purposes. The lack of dynamism in a language like ReScript can be solved with metaprogramming. PPX’s are actually pretty fast, the slow ones are slow because the execution happens via a nodejs script with slow startup times. I agree though that PPX is perhaps not the best implementation, language level macros are a better solution, but they are still in the research phase in OCaml.

ReScript also still runs an ancient implementation of PPX’s, so upgrading that would also help performance.

4 Likes

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