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:
- get database connection (() -> Task Connection)
- find account (Connection -> Task Account)
- 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)
})