A task has a few steps, if each step's input is only from direct last step, it is easy. However, more often, some steps are depend on not only the direct last step.

I can work out via several ways, but all end up with ugly nested code, I hope anyone could help me to find better ways.

I created the following signIn-like example to demonstrate, the process has 3 steps as below:

  1. get database connection (() -> Task Connection)
  2. find account (Connection -> Task Account)
  3. create token (Connection -> accountId -> Task Token)

#step3 depends not only on step#2 but also step#1.

The below are the jest unit tests by using folktale2

import {task, of} from 'folktale/concurrency/task'
import {converge} from 'ramda'

const getDbConnection = () =>
    task(({resolve}) => resolve({id: `connection${Math.floor(Math.random()* 100)}`})
)

const findOneAccount = connection =>
    task(({resolve}) => resolve({name:"ron", id: `account-${connection.id}`}))

const createToken = connection => accountId =>
    task(({resolve}) => resolve({accountId, id: `token-${connection.id}-${accountId}`}))

const liftA2 = f => (x, y) => x.map(f).ap(y)

test('attempt#1 pass the output one by one till the step needs: too many passing around', async () => {
    const result = await getDbConnection()
        .chain(conn => findOneAccount(conn).map(account => [conn, account.id])) // pass the connection to next step
        .chain(([conn, userId]) => createToken(conn)(userId))
        .map(x=>x.id)
        .run()
        .promise()

    console.log(result) // token-connection90-account-connection90
})

test('attempt#2 use ramda converge and liftA2: nested ugly', async () => {
    const result = await getDbConnection()
        .chain(converge(
            liftA2(createToken),
            [
                of,
                conn => findOneAccount(conn).map(x=>x.id)
            ]
        ))
        .chain(x=>x)
        .map(x=>x.id)
        .run()
        .promise()

    console.log(result) // token-connection59-account-connection59
})

test('attempt#3 extract shared steps: wrong',  async () => {
    const connection = getDbConnection()

    const accountId = connection
    .chain(conn => findOneAccount(conn))
    .map(result => result.id)

    const result = await of(createToken)
    .ap(connection)
    .ap(accountId)
    .chain(x=>x)
    .map(x=>x.id)
    .run()
    .promise()

    console.log(result) // token-connection53-account-connection34, wrong: get connection twice
})
  • attempt#1 is right, but I have to pass the output of very early step till the steps need it, if it is across many steps, it is very annoying.

  • attempt#2 is right too, but end up with nested code.

  • I like attempt#3, it use some variable to hold the value, but unfortunately, it doesn't work.

Update-1 I am think another way to put all outputs into a state which will pass through, but it may very similar attempt#1

test.only('attempt#4 put all outputs into a state which will pass through',  async () => {
    const result = await getDbConnection()
    .map(x=>({connection: x}))
    .map(({connection}) => ({
        connection,
        account: findOneAccount(connection)
    }))
    .chain(({account, connection})=>
        account.map(x=>x.id)
        .chain(createToken(connection))
    )
    .map(x=>x.id)
    .run()
    .promise()


    console.log(result) //     token-connection75-account-connection75
})

update-2 By using @Scott's do approach, I am pretty satisfied with the below approach. It's short and clean.

test.only('attempt#5 use do co', async () => {
    const mdo = require('fantasy-do')

    const app = mdo(function * () {
        const connection = yield getDbConnection()
        const account =  yield findOneAccount(connection)

        return createToken(connection)(account.id).map(x=>x.id)
    })

    const result = await app.run().promise()

    console.log(result)
})
1

There are 1 answers

3
Scott Christopher On BEST ANSWER

Your example could be written as follows:

const withConnection = connection =>
  findOneAccount(connection)
      .map(x => x.id)
      .chain(createToken(connection))

getDbConnection().chain(withConnection)

This is similar to your second attempt, though makes use of chain rather than ap/lift to remove the need for the subsequent chain(identity). This could also be updated to use converge if you want, though I feel it loses a great amount of readability in the process.

const withConnection = R.converge(R.chain, [
  createToken,
  R.compose(R.map(R.prop('id')), findOneAccount)
])

getDbConnection().chain(withConnection)

It could also be updated to look similar to your third attempt with the use of generators. The follow definition of the Do function could be replaced by one of the existing libraries that offers some form of "do syntax".

// sequentially calls each iteration of the generator with `chain`
const Do = genFunc => {
  const generator = genFunc()
  const cont = arg => {
    const {done, value} = generator.next(arg)
    return done ? value : value.chain(cont)
  }
  return cont()
}

Do(function*() {
  const connection = yield getDbConnection()
  const account = yield findOneAccount(connection)
  return createToken(connection)(account.id)
})