DEV Community

Cover image for Async await like syntax, without ppx in Rescript !!!
Praveen
Praveen

Posted on • Originally published at blog.techrsr.com

Async await like syntax, without ppx in Rescript !!!

Prerequisite:

  • Basic understanding of functional programming.
  • Basic knowledge on Rescript/ReasonML.

The code and ideas that I will be discussing about in this article are my own opinions. It doesn't mean this is the way to do it, but it just means that this is also a way to do it. Just my own way.

In Rescript, currently (at the time of writing this article) there is no support for async/await style syntax for using promises. You can read about it here. Even though Rescript's pipe syntax make things cleaner and more readable, when it comes to working with promises there is still readability issues due to the lack of async/await syntax. There are ppx available to overcome this issue. But what if, we can overcome this issue without using any ppx.

Lets first look at, how existing promise chaining looks like in Rescript.

fetchAuthorById(1)
  |> Js.Promise.then_(author => {
    fetchBooksByAuthor(author) |> Js.Promise.then_(books => (author, books))
  })
  |> Js.Promise.then_(((author, books)) => doSomethingWithBothAuthorAndBooks(author, books))

Enter fullscreen mode Exit fullscreen mode

In the above code, to access both author and books, I am creating a tuple to be passed into the next promise chain and using it there. This can easily grow and become more cumbersome when we chain three or more levels.

The idea is to,

Create a function that takes multiple promise functions as labelled arguments, executes them sequentially and stores each result as a value in an object, with labels as keys of the object

This idea is inspired from Haskell's DO notation. Lets see, how this function looks like.

type promiseFn<'a, +'b> = 'a => Js.Promise.t<'b>

let asyncSequence = (~a: promiseFn<unit, 'a>, ~b: promiseFn<{"a": 'a}, 'b>) =>
  a()
  |> Js.Promise.then_(ar => {"a": ar}->Js.Promise.resolve)
  |> Js.Promise.then_(ar =>
    ar
    ->b
    ->map(br =>
      {
        "a": ar["a"],
        "b": br,
      }
    )
  )
Enter fullscreen mode Exit fullscreen mode

Lets understand what this function is doing.

  1. A type called promiseFn is defined, that takes some polymorphic type 'a and returns a promise of type 'b.
  2. asyncSequence function takes two labelled arguments a and b which are of type promiseFn.
  3. Argument a is a function that takes nothing, but returns a promise of 'a.
  4. Argument b is a function that takes an Object of type {"a": 'a} where the key a corresponds to the label a and the value 'a corresponds to the response of the function a.
  5. a is first invoked and from its response an Object of type {"a": 'a} is created and passed into function b. The response of function b is taken and an object of type {"a": 'a, "b": 'b} is created.

The above function, chains only 2 promise functions. But, using this method we can create functions that chains multiple promise functions.

// Takes 3 functions
let asyncSequence3 = (
  ~a: promiseFn<unit, 'a>,
  ~b: promiseFn<{"a": 'a}, 'b>,
  ~c: promiseFn<{"a": 'a, "b": 'b}, 'c>,
) =>
  asyncSequence(~a, ~b) |> Js.Promise.then_(abr =>
    abr->c
      |> Js.Promise.then_(cr =>
        {
          "a": abr["a"],
          "b": abr["b"],
          "c": cr,
        }->Js.Promise.resolve
      )
  )

// Takes 4 functions
let asyncSequence4 = (
  ~a: promiseFn<unit, 'a>,
  ~b: promiseFn<{"a": 'a}, 'b>,
  ~c: promiseFn<{"a": 'a, "b": 'b}, 'c>,
  ~d: promiseFn<{"a": 'a, "b": 'b, "c": 'c}, 'd>,
) =>
  asyncSequence3(~a, ~b, ~c) |> Js.Promise.then_(abcr =>
    abcr->d
      |> Js.Promise.then_(dr =>
        {
          "a": abcr["a"],
          "b": abcr["b"],
          "c": abcr["c"],
          "d": dr,
        }->Js.Promise.resolve
      )
  )

// .... Any level
Enter fullscreen mode Exit fullscreen mode

See, we are using previous asyncSequence3 to define next level asyncSequence4. To understand this function better, lets see how it is used. Lets rewrite our previous example using this asyncSequence4.

asyncSequence4(
  ~a=() => fetchAuthorById(1),
  ~b=arg => fetchBooksByAuthor(arg["a"]),
  ~c=arg => doSomethingWithBothAuthorAndBooks(arg["a"], arg["b"]),
  ~d=arg => Js.log(arg)->Js.Promise.resolve
)

// Response of asyncSequence4 will be a promise of type
// {
//  "a": <Author>,
//  "b": <BooksArray>,
//  "c": <Response of doSomethingWithBothAuthorAndBooks>
//  "d": <unit, since Js.log returns unit>
// }
Enter fullscreen mode Exit fullscreen mode

What is happening is, the response of fetchAuthorById is taken and an object of type {"a": <Author>} is created. This object is passed to function b as arg and hence that function b has access to previous function a's result. Now the response of b is merged together with response of a into a single object as {"a": <Author>, "b": <BooksArray>} and passed to function c as argument arg. Now function c has access to both the response of a as well as response of b in the object that is received as argument. This is continued down the path to function d.

With this approach the chaining is easy and multiple asyncSequence can be chained like below, which can provide access to all the previous values.

let promiseResp = asyncSequence4(
  ~a=() => fetchAuthorById(1),
  ~b=arg => fetchBooksByAuthor(arg["a"]),
  ~c=arg => doSomethingWithBothAuthorAndBooks(arg["a"], arg["b"]),
  ~d=arg => Js.log(arg)->Js.Promise.resolve
)

asyncSequence(
  ~a=() => promiseResp,
  ~b=arg => doSomethingWithAllThePreviousResponse(arg["a"])
)
Enter fullscreen mode Exit fullscreen mode

Before we jump into pros and cons of this approach, lets see one common mistake that can happen.

asyncSequence3(
  ~a=() => firstExecution(),
  ~c=_ => thirdExecution(),
  ~b=_ => secondExecution(),
)
Enter fullscreen mode Exit fullscreen mode

The above code will compile fine. It is easy to think that c will be executed after a, but thats not true.
The execution will always happen from a to z even though the order is changed.

Now, lets see what are the pros and cons of this approach.

Pros:

  1. Far more readable than the raw Promise chaining.
  2. Somewhat similar to the js async await syntax.
  3. Each function down the line has access to all the previous responses.
  4. No PPX and no additional dependencies needed.
  5. Completely type safe. Compiler will raise errors of any wrong usage.
  6. One asyncSequence can be chained to the next asyncSequence easily.

Cons:

  1. Multiple overloaded functions required.
  2. Order must not be changed.
  3. Keys of the object cannot be changed ("a", "b" ... will always be the keys).

You can check the refactored, full code here.

Hope you enjoyed! Happy Hacking!

Oldest comments (2)

Collapse
 
tsnobip profile image
Paul Tsnobiladzé

why using labeled functions here? Since order has a meaning here, why not just use unlabeled arguments?

Collapse
 
praveenkumarrr profile image
Praveen • Edited

You can think of it like an assignment statement.
let a = await fetchAuthorById(1) // Js
~a=fetchAuthorById(1)
More importantly, the labels are used as keys in the object that is being passed into the next function. Next function can access previous result with arg["a"]. It helps to remind at which key the result is stored.