Async await like syntax, without ppx in Rescript !!!
Praveen
Posted on September 29, 2021
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))
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,
}
)
)
Lets understand what this function is doing.
- A
type
calledpromiseFn
is defined, that takes some polymorphic type'a
and returns a promise of type'b
. -
asyncSequence
function takes two labelled argumentsa
andb
which are of typepromiseFn
. - Argument
a
is a function that takes nothing, but returns a promise of'a
. - Argument
b
is a function that takes anObject
of type{"a": 'a}
where the keya
corresponds to the labela
and the value'a
corresponds to the response of the functiona
. -
a
is first invoked and from its response anObject
of type{"a": 'a}
is created and passed into functionb
. The response of functionb
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
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>
// }
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"])
)
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(),
)
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:
- Far more readable than the raw Promise chaining.
- Somewhat similar to the js async await syntax.
- Each function down the line has access to all the previous responses.
- No PPX and no additional dependencies needed.
- Completely type safe. Compiler will raise errors of any wrong usage.
- One
asyncSequence
can be chained to the nextasyncSequence
easily.
Cons:
- Multiple overloaded functions required.
- Order must not be changed.
- 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!
Posted on September 29, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.