Используя функциональное программирование javascript с folktale2, как изящно получить доступ к результатам предыдущих задач?
Задача имеет несколько шагов, если ввод каждого шага производится только с последнего последнего шага, это легко. Однако, чаще всего, некоторые шаги зависят не только от прямого последнего шага.
Я могу работать несколькими способами, но все они заканчиваются уродливым вложенным кодом, я надеюсь, что любой сможет помочь мне найти лучшие способы.
Я создал следующий пример, похожий на signIn, чтобы продемонстрировать, что процесс состоит из 3 шагов, как показано ниже:
- получить соединение с базой данных (() -> Task Connection)
- найти учетную запись (Подключение -> Задача Учетная запись)
- создать токен (Connection -> accountId -> Task Token)
# 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, она использует некоторую переменную для хранения значения, но, к сожалению, она не работает.
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
})
обновление-2 с помощью @Scott's 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)
})
1 ответ
Ваш пример может быть написан следующим образом:
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
Функция может быть заменена одной из существующих библиотек, которая предлагает некоторую форму "сделать синтаксис".
// 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)
})