任务有几个步骤,如果每个步骤的输入仅来自直接的最后一步,则很容易。但是,更常见的是,某些步骤不仅取决于直接的最后一步。
我可以通过多种方式解决问题,但最终都会出现丑陋的嵌套代码,我希望有人能帮助我找到更好的方法。
我创建了以下类似signIn的示例来演示,该过程包含以下3个步骤:
#step3不仅取决于步骤#2,还取决于步骤#1。
以下是使用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
})
尝试#1是正确的,但我必须通过非常早的步骤输出,直到步骤需要它,如果它跨越很多步骤,那就非常烦人。
尝试#2也是对的,但最后是嵌套代码。
我喜欢尝试#3,它使用一些变量来保存值,但不幸的是,它不起作用。
更新-1 我认为另一种方法是将所有输出置于将要通过的状态,但它可能非常类似于尝试#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
})
更新-2
通过使用@ Scott的do
方法,我对以下方法非常满意。它简短而干净。
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)
})
答案 0 :(得分:2)
你的例子可以写成如下:
const withConnection = connection =>
findOneAccount(connection)
.map(x => x.id)
.chain(createToken(connection))
getDbConnection().chain(withConnection)
这类似于您的第二次尝试,但使用chain
而不是ap
/ lift
来消除对后续chain(identity)
的需求。如果您愿意,也可以将其更新为使用converge
,但我觉得它在此过程中失去了大量的可读性。
const withConnection = R.converge(R.chain, [
createToken,
R.compose(R.map(R.prop('id')), findOneAccount)
])
getDbConnection().chain(withConnection)
它也可以更新为类似于使用生成器的第三次尝试。 Do
函数的以下定义可以由现有的库之一替换,这些库提供某种形式的" 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)
})