通过 folktale2 使用函数式编程 javascript,如何优雅地访问先前任务的结果?
By using functional programming javascript with folktale2, how to access results of previous tasks gracefully?
一个任务有几个步骤,如果每个步骤的输入直接来自最后一步,就很容易了。然而,更多的时候,有些步骤不仅依赖于直接的最后一步。
我可以通过多种方式解决,但都以丑陋的嵌套代码告终,我希望有人能帮助我找到更好的方法。
我创建了以下类似登录的示例来演示,该过程分为以下 3 个步骤:
- 获取数据库连接(() -> 任务连接)
- 查找帐户(连接 -> 任务帐户)
- 创建令牌(连接 -> accountId -> 任务令牌)
#step3不仅依赖step#2还依赖step#1.
下面是使用folktale2的jest单元测试
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是对的,但是我必须通过很早一步的输出,直到步骤需要它,如果它跨越很多步骤,那就很烦人了。
尝试#2 也是正确的,但以嵌套代码结束。
我喜欢尝试#3,它使用一些变量来保存值,但不幸的是,它不起作用。
Update-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
})
update-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)
})
你的例子可以这样写:
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)
})
一个任务有几个步骤,如果每个步骤的输入直接来自最后一步,就很容易了。然而,更多的时候,有些步骤不仅依赖于直接的最后一步。
我可以通过多种方式解决,但都以丑陋的嵌套代码告终,我希望有人能帮助我找到更好的方法。
我创建了以下类似登录的示例来演示,该过程分为以下 3 个步骤:
- 获取数据库连接(() -> 任务连接)
- 查找帐户(连接 -> 任务帐户)
- 创建令牌(连接 -> accountId -> 任务令牌)
#step3不仅依赖step#2还依赖step#1.
下面是使用folktale2的jest单元测试
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是对的,但是我必须通过很早一步的输出,直到步骤需要它,如果它跨越很多步骤,那就很烦人了。
尝试#2 也是正确的,但以嵌套代码结束。
我喜欢尝试#3,它使用一些变量来保存值,但不幸的是,它不起作用。
Update-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
})
update-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)
})
你的例子可以这样写:
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)
})