通过使用与folktale2的函数式编程javascript,如何优雅地访问以前任务的结果?

时间:2017-09-06 08:24:37

标签: javascript functional-programming ramda.js folktale fantasyland

任务有几个步骤,如果每个步骤的输入仅来自直接的最后一步,则很容易。但是,更常见的是,某些步骤不仅取决于直接的最后一步。

我可以通过多种方式解决问题,但最终都会出现丑陋的嵌套代码,我希望有人能帮助我找到更好的方法。

我创建了以下类似signIn的示例来演示,该过程包含以下3个步骤:

  1. 获取数据库连接(() - >任务连接)
  2. 查找帐户(连接 - >任务帐户)
  3. 创建令牌(连接 - > accountId - >任务令牌)
  4. #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)
    })
    

1 个答案:

答案 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)
})