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)
})
Your example could be written as follows:
This is similar to your second attempt, though makes use of
chain
rather thanap
/lift
to remove the need for the subsequentchain(identity)
. This could also be updated to useconverge
if you want, though I feel it loses a great amount of readability in the process.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".