Используя функциональное программирование javascript с folktale2, как изящно получить доступ к результатам предыдущих задач?

Задача имеет несколько шагов, если ввод каждого шага производится только с последнего последнего шага, это легко. Однако, чаще всего, некоторые шаги зависят не только от прямого последнего шага.

Я могу работать несколькими способами, но все они заканчиваются уродливым вложенным кодом, я надеюсь, что любой сможет помочь мне найти лучшие способы.

Я создал следующий пример, похожий на signIn, чтобы продемонстрировать, что процесс состоит из 3 шагов, как показано ниже:

  1. получить соединение с базой данных (() -> Task Connection)
  2. найти учетную запись (Подключение -> Задача Учетная запись)
  3. создать токен (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)
})
Другие вопросы по тегам