Skip to content

Demo of bs-let on umami (=async/await)

Rémy El Sibaïe requested to merge demo-ppx-let into develop

Context

Asynchronous calls in umami are currently handled by Future library. The library is pretty neat and even provides a >>= operator. However, because of refmt, infix operators are almost impossible to use in reasonml because parenthesis are added everywhere and chaining them with function calls creates nested code.

The current solution proposes to include bs-let in our dependencies and replace call to

asyncComputation()
->Future.flatMapOk(v => somethingAsyncWith(v))
->Future.mapOk(res => somethingWith(res));

by

let%F v = asyncComputation(); 
let%F res = somethingAsyncWith(v);
somethingWith(res)->FutureEx.ok;

It would be also possible to use map instead of flatMap to avoid the final conversion to Future.t.

let%F v = asyncComputation(); 
let%Fmap res = somethingAsyncWith(v);
somethingWith(res);

Writing futures in a more sequential manner

It doesn't bring that much it terms of code size even if the tendency is for it to be reduced. The actual win is in term of code readability. The semantic information given by the variable names is easier to read and there is more emphasis over the different steps.

Before, Future.<functionName> patterns were taking a lot of space on the screen and hid the names (below alias, etc) in nested blocks. Also sometimes, several nesting levels were needed when they are not needed anymore with let-binders.

I feel that the complexity brought by a new syntax is not much as it is really easy to learn. It is close to async/await in JS and known by a lot of developers. The value it brings to day-to-day development is way stronger than the weight it adds on explaining what %F (or whatever we choose) is.

This extension could also handle more modules with map/flatMap operation: Option, etc. We could also enforce some things like the error type of our futurs if we want to homogeneize (it is planned for a futur refactoring).

Discussion

I would like to discuss with the dev team:

  • Whether or not we should do this change ?
  • What would we choose for the extension: %F? %Future? %Await?
  • What about other function that flatMap ? map ?
  • Any other proposal...

Regarding ReScript compatibility, honestly I don't care. https://github.com/reasonml-labs/bs-let/issues/30

It is not compatible, but a language that admittedly removes every features because they are too complicated does not make me want to use it's restrictive syntax. The price of moving from Reason-syntax to ReScript-syntax is currently high (editors, ocaml features lost, ppx lost) when it brings nothing except arbitrary preferences of what a language syntax should be. Their goal is to keep Reason-syntax. Well fine.

Demo on TaquitoAPI.Transfer.Estimate.batch

    let batch =
        (
          ~endpoint,
          ~baseDir,
          ~source: PublicKeyHash.t,
          ~transfers,
          (),
        ) => {
      Wallet.aliasFromPkh(~dirpath=baseDir, ~pkh=source, ())
      ->Future.flatMapOk(alias =>
          Wallet.pkFromAlias(~dirpath=baseDir, ~alias, ())
        )
      ->Future.map(convertWalletError)
      ->Future.mapOk(pk => {
          let tk = Toolkit.create(endpoint);
          let signer =
            EstimationSigner.create(~publicKey=pk, ~publicKeyHash=source, ());
          let provider = Toolkit.{signer: signer};
          tk->Toolkit.setProvider(provider);
          tk;
        })
      ->Future.flatMapOk(tk =>
          endpoint
          ->transfers(source)
          ->Future.map(ResultEx.collect)
          ->Future.flatMapOk(txs =>
              tk.estimate
              ->Toolkit.Estimation.batch(
                  txs
                  ->List.map(tr => {...tr, kind: opKindTransaction})
                  ->List.toArray,
                )
              ->convertToErrorHandler
            )
        )
      ->Future.flatMapOk(r =>
          revealFee(~endpoint, source)
          ->Future.mapOk(revealFee => (r, revealFee))
        );
    };

===>

   let batch =
        (~endpoint, ~baseDir, ~source: PublicKeyHash.t, ~transfers, ()) => {
      let%F alias =
        Wallet.aliasFromPkh(~dirpath=baseDir, ~pkh=source, ())
        ->Future.map(convertWalletError);

      let%F pk =
        Wallet.pkFromAlias(~dirpath=baseDir, ~alias, ())
        ->Future.map(convertWalletError);

      let tk = Toolkit.create(endpoint);
      let signer =
        EstimationSigner.create(~publicKey=pk, ~publicKeyHash=source, ());
      let provider = Toolkit.{signer: signer};
      tk->Toolkit.setProvider(provider);

      let%F txs = endpoint->transfers(source)->Future.map(ResultEx.collect);
      let%F res =
        tk.estimate
        ->Toolkit.Estimation.batch(
            txs
            ->List.map(tr => Toolkit.{...tr, kind: opKindTransaction})
            ->List.toArray,
          )
        ->convertToErrorHandler;

      let%F revealFee = revealFee(~endpoint, source);
      (res, revealFee)->FutureEx.ok;
    };

As you can see in the last version, there is almost 0 code nesting. Steps are sequentially written and well separated. Also variable names at the beginning of each step makes it clear what the expected result is.

Edited by Rémy El Sibaïe

Merge request reports