question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

A question on Async and applicatives

See original GitHub issue

I am going to outline the nesting problem with promises and how I imagined they would be solved with crocks in this issue

This is my honest process in trying to find a solution. The answer may simply be to use do /async await.

Here is an example of promise nesting taken from https://gist.github.com/MaiaVictor/bc0c02b6d1fbc7e3dbae838fb1376c80

function getBalances() {
  return web3.eth.accounts()
    .then(accounts => Promise.all(accounts.map(web3.eth.getBalance).then(balances => [accounts, balances])))
    .then(([accounts, balances]) => Ramda.zipObject(accounts, balances));
}

When I saw this I first thought that crocks could provide an elegant solution with applicative like this:

Async.fromPromise(web3.eth.accounts)
  .ap(Async.all(map(Async.fromPromise(web3.eth.getBalance))))
  .fork(console.error,  Ramda.zipObject)

Where applicative provides the current data in the flow and applicative adds new data to the chain.

But applicatives don’t work like this, so my question is:

Is there simply no easy way to avoid nested data calls for use cases when you wish to add extra data to the flow?

Issue Analytics

  • State:closed
  • Created 5 years ago
  • Comments:7 (5 by maintainers)

github_iconTop GitHub Comments

7reactions
evilsoftcommented, Jul 19, 2018

So the best way to solve a problem like this is chunk by chunk. You were 💯 that the solution lies around applicative, just in (2) different ways then you may have been thinking of. I will break down a solution to this problem step by step.

Before we start with the code we will bring some stuff in from crocks:

const {
  Assign, Async, compose, fanout, liftA2, merge,
  mreduce, objOf, traverse
} = require('crocks')

const { fromPromise, of: asyncOf } = Async

So our Promise functions need to be 🌽verted to Async functions…Also going to bind incase that dep uses internal state (aws and many others need to be bound):

// getAccounts :: () -> Async Error [ String ]
const getAccounts = fromPromise(
  web3.eth.accounts.bind(web3.eth)
)

// getBalance :: String -> Async Error Number
const getBalance = fromPromise(
  web3.eth.getBalance.bind(web3.eth)
)

At the heart of the problem we need to retain a some data for use later. Anytime you have parallel paths (one to process and one to retain (or process in another way if you wanna)), you can think of Product Types, or in this case the humble Pair.

Instead of working directly with a Pair, because we want to share a value between (2) paths, we can use a function called fanout. This will take a value and send it to (2) provided functions and return their results in a new Pair. That getBalance will return an Async so we want the String on the left lifted into an Async as is, using our asyncOf function. It is important that it is on the left, as we will be using objOf which takes the key on the left. Doing the following and passing a String will give back a Pair with Asyncs in both sides key on the left, value on the right:

// fn :: String -> Pair (Async e String) (Async Error Number)
const fn =
  fanout(asyncOf, getBalance)

Now that we have our Pair of Async we can use the Applicative to combine them under an operation using liftA2 that will lift a function into an Async (in this case) and apply both Async to the function, returning a new Async that is the result. In this case that function is objOf as it will make a new object with the accountId as the key and the balance as the value.

BUT we need to take them out of the Pair and give them to liftA2. That is where merge comes into play. It takes a binary function (liftA2(objOf)) and applies the left side of the Pair as the first argument and the right side as the second:

// fn ::  Pair (Async e String) (Async Error Number) -> Async Error Object
const fn =
  merge(liftA2(objOf))

Now that those online, we can build a composition that gives us a function of the form String -> Async Error Object that will build a balance for (1) account:

// accountBalance :: String -> Async Error Object
const accountBalance = compose(
  merge(liftA2(objOf)),
  fanout(AsyncOf, getBalance)
)

Perfect! The only bummer is that it only works for one. So another way we can use the Async as an Applicative is to take advantage of Array being traversable. Because getAccounts returns an Array of Strings ([ String ]), we can just chain in a traverse with the accountBalance function and get something like:

getAccounts()
  .chain(traverse(Async, accountBalance))

Now we are almost there, that will return an Array of Balance Objects, looks like you want one object as the result, so we can use the Assign Monoid with mreduce (which extracts to a value of the carrier type of a given Monoid. with our final flow being:

getAccounts()
  .chain(traverse(Async, accountBalance))
  .map(mreduce(Assign))

With it all together, it looks like this:

const {
  Assign, Async, compose, fanout, liftA2, merge,
  mreduce, objOf, traverse
} = require('crocks')

const { fromPromise, of: asyncOf } = Async

// getAccounts :: () -> Async Error [ String ]
const getAccounts = fromPromise(
  web3.eth.accounts.bind(web3.eth)
)

// getBalance :: String -> Async Error Number
const getBalance = fromPromise(
  web3.eth.getBalance.bind(web3.eth)
)

// accountBalance :: String -> Async Error Object
const accountBalance = compose(
  merge(liftA2(objOf)),
  fanout(AsyncOf, getBalance)
)

getAccounts()
  .chain(traverse(Async, accountBalance))
  .map(mreduce(Assign))

Although it seems like a lot, there is a lot of potential for reusability throughout your app. each step is in its own function which can be used in other places and not in a nested Promise.

Hope this helps…Maybe I should do a live code on using Applicatives in this fashion?

0reactions
evilsoftcommented, Jul 30, 2018

Awesome. Closing!

Read more comments on GitHub >

github_iconTop Results From Across the Web

Can Applicative Functors make async Tasks more effective?
I don't know Folktale's Task but usually the applicative in the context of asynchronous computations runs "in parallel", because the next ...
Read more >
Interview question: async & await (C#) - DEV Community ‍ ‍
Q: What is the purpose of async / await keywords? These keywords allow writing asynchronous non-blocking code in a synchronous fashion.
Read more >
Chapter 13. Working with asynchronous computations
Using Task to represent asynchronous computations; Composing asynchronous operations sequentially and in parallel; Traversables: handling lists of elevated ...
Read more >
What are these applicative functors you speak of? | Devlog
They are asynchronous, so you might be tempted to use Async/await but that wouldn't be the best idea. Those two don't depend on...
Read more >
async: Asynchronous and Concurrent Programming
Exercise: what will this program output? The Concurrently newtype wrapper uses the concurrently function to implement its Applicative instance, and race to ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found