Promise and Async in the (Elmish) wild

Promise and Async in the (Elmish) wild

May 17, 2023

About Async and Promise

F# has applied async/await pattern years before C# and TypeScript.

We can nicely await prolonged or external actions with async or task computation expressions in a .NET application. We can use only the first with Fable as the web is still mostly oriented at asynchronous programming. Parallel programming can be emulated with web workers to some extent. However, we can expect true parallelism with various wasm implementations and proposals at some point.

In the JS world, we initially had only Promises and CPS that support asynchronous programming. Promises provide a way to handle the outcome of asynchronous operations using methods like .then() and .catch(). These methods allow you to attach callback functions to execute when the library author fulfills or rejects the promise.

With time we got our well-known async/await, which is compatible with Promises too.

However, we still sometimes need to create Promises directly, as not every asynchronous library provides promises in responses. Not everything is also thenable, and many libraries are designed with plain CPS (Continuation Passing Style).

Practice

1) using Promise to gather geolocation data

With my experimental web application that addresses some geospatial analyses, we can classify various places (in the example, I'm showing F1 circuits in Europe and Job Offers in London) based on their context to one or many groups: family members, friends, colleagues, etc. On init, the web application starts with some made-up groups, but users can remove and add new members with Mapbox Address Autofill, which precisely describes their location:

Currently, the application does not preserve any state between sessions, so anyone always has to provide their data.

One of the alternatives is to use Web Browser Geolocation API. On-page load, the application can get the user approval to infer his location, which with some accuracy is sufficient for the analyses, yet is not perfect enough to be threatened as personal data.

Plenty of external JS libraries make this browser-native functionality more friendly, as in the original form, it is based on callbacks:

With Fable, we don't have to use an external library here. We can construct a Promise inside the expected callback definitions and await that Promise to dispatch further actions.

In my application, I use Elmish (useElmish) library from the Feliz umbrella. It means I declaratively provide what message, with what data, should go when the promise is resolved.

Elmish style promotes a unidirectional data flow pattern, immutability, and pure functions to manage state and update the UI components efficiently.

Let's create a Promise from the geolocation native function with Fable

geolocated function returns a JS.Promise, which at some point in time, feeds the application with an option of localization coordinates.

For simplicity, I do not bother with the possible exception (promise reject) now. For that, we should use as the output Result<(float*float) option>) instead of the rising exception.

The outcome is cool: I can expect coordinates in a unidirectional flow that will not introduce flaws with my Elmish style. Now it is time to consume this data.

First, I need to add a new kind of expected message that will curry my expected location data:

Interception of user geolocation data should start immediately, and the elmish init function is a good place for it. It returns an initial state and commands that should be fired immediately:

Next, our SetGeolocatedUser, along with coordinates, will entry the elmish update function, where a new crew consisting of a single person with the name "You" will be created and set as current.

Our application now looks like follows on startup ( I hardcoded 2s delay to make this effect visible):

Note: presented approach is not the only possible way. In a regular Elmish (not the useElmish hook from Feliz) we could use subscription command and directly dispatch our message without creating a JS.Promise. Similarly we could dispatch it inside useEffect hook. But I found the Promise combined with regular Elmish messages the most native and predictive one, especially when the application grows.

2) Using Async to gather Trip Advisor Content data

Async and Promise in Fable are in the vast majority consistent and interchangeable, and we can combine and chain them as we wish in F# way:

But it is Async one that is natively related to F#/.NET. Hence it has more combinators that enable more features, like keeping track of the operation status. It is very beneficial in both the update function of Elmish and creating the view (UI) from the application state.

To achieve that desired effect of keeping track of the async status visually, we need two types: one to be used to update the application state (message union case) and one that reflects the current state after the message (which in turn impacts the view):

Note: this approach enforces much more code, especially in the update function, but "complicates" the view model too. For FP newcommers it is often seen as a burden and hard to follow, frequently discuraging to use Elmish style at all.

But once it is understood by heart, it pays off in a non-trivial applications. It sagnificantly improves reasoning about your application data flow and makes updates much easier and predictable with time.

We need a client fetching the Trip Advisor data. Here goes the most important part of it:

The nearby response has a type that is scary to many :

For particular location coordinates, it asynchronously returns a collection of place information with appropriate F# wrappers to handle success or failure, thanks to an amazing set of Thoth's libraries.

Initially, this definition can be hard to follow, but after gaining some experience, it is plain English.

Knowing how to call for content data, let's focus on application messages and the state. Our message definition needed to request such data can look like this:

This message union case conveys both request arguments and the status response, which changes from Starter to InProgress, and either Error or Resolved. Here is the part of the update function that depicts how it changes:

This is unique, as when the async call is resolved, the same message what initiated the call is dispatched again with a new status. Again, if you see this for the very first time, it takes some time to grasp what is happening. But when you are more into the pattern, your reasoning about application flow is great.

To request the data with "Started" status, we need to dispatch that message on any onClick event, whose view has some location data tied:

Reflecting the state in the view is very clear:

And the final result:

Enjoy this post?

Buy Paweł Stadnicki a coffee

More from Paweł Stadnicki