在 fp-ts 中分支和合并

Branch and merge in fp-ts

我有以下pipe


pipe(
  getProduct(), //step 1
  chain((it) => E.right(Object.assign({}, it, { tax: 0.1 }))), //step 2
  chain((it) => E.right(Object.assign({}, it, { delivery: 0.15 }))), //step 3
  //chain((it) => E.left('ERROR')), //step 4
  E.fold( //final step
    (e) => {
      console.log(`error: ${e}`)
    },
    (it) => {
      console.log(
        `ok ${it.count} ${it.pricePerItem} ${it.tax} ${it.delivery}`
      )
    }
  )
)

哪里

getProduct = () => E.right({ count: 10, pricePerItem: 5 })

步骤 1 产生输出 { count: 10, pricePerItem: 5 }

步骤 2 将步骤 1 的输出作为输入并产生输出 { count: 10, pricePerItem: 5, tax: 0.1 }

第 3 步采用第 2 步的输出 { count: 10, pricePerItem: 5, tax: 0.1 } 并产生输出 { count: 10, pricePerItem: 5, tax: 0.1, delivery: 0.15 }

第 4 步只是一个占位符,它可能会生成一个 left 来指示错误情况。我只是把它遗漏了。

它在 pipe 中按预期工作。但我不想那样。

我希望第 2 步采用输入 { count: 10, pricePerItem: 5 } 并向其添加 tax。同时,我希望第 3 步采用相同的输入 { count: 10, pricePerItem: 5 } 并向其添加 delivery

然后我希望第 4 步获取第 2 步和第 3 步的输出并将它们合并回来。

我看到了一些涉及 bind and/or do notation 的东西,比如这个 但不太确定。

那么如何将流分支和合并,而不是总是在管道中 运行?

更新 命令式编程中的等价物如下:

const products = getProducts() 
const productsWithTax = getProductsWithTax(products)
const productsWithDelivery = getProductsWithDelivery(products)

const productsWithTaxAndDelivery = getWithTaxAndDelivery(productsWithTax, productsWithDelivery)

重点是我不想要 true 管道。

这里绝对可以使用do表示法。这是一个符合您的要求的解决方案:

import * as E from 'fp-ts/Either';
import { Either } from 'fp-ts/Either'
import { pipe } from 'fp-ts/function'

// Your base product type
interface Product {
  count: number
  pricePerItem: number
}

// Something that has a tax added
interface Tax {
  tax: number
}

// Something that has a delivery added
interface Delivery {
  delivery: number
}

declare function getProduct(): Either<Error, Product>

declare function getTax(p: Product): Either<Error, Product & Tax>

declare function getDelivery(p: Product): Either<Error, Product & Delivery>

function solution1(): Either<Error, Product & Tax & Delivery> {
  return pipe(
    // begin the `do` notation
    E.Do,
    // (1)
    // bind your product to a variable `p`
    E.bind('p', getProduct),
    // (2)
    // use your product to get your product & tax, and bind that to `pTax`
    E.bind('pTax', ({ p }) => getTax(p)),
    // (3)
    // use your product to get your product & delivery, and bind that to `pDelivery`
    E.bind('pDelivery', ({ p }) => getDelivery(p)),
    // (4)
    // merge your original product, product w/tax, and product w/delivery
    E.map(({ p, pTax, pDelivery }) => ({ ...p, ...pTax, ...pDelivery }))
  );
}

诚然,这里有一些不必要的 return 类型重叠,我们不得不在最后笨拙地合并对象。您可以 return taxdelivery 作为独立结果,而不是在函数之间 returning 扩展对象,并在最后合并所有内容:

declare function getProduct(): Either<Error, Product>

declare function getTax(p: Product): Either<Error, number> // changed

declare function getDelivery(p: Product): Either<Error, number> // changed

function solution2(): Either<Error, Product & Tax & Delivery> {
  return pipe(
    E.Do,
    // get product
    E.bind('p', getProduct),
    // use product to get tax
    E.bind('tax', ({ p }) => getTax(p)),
    // use product to get delivery
    E.bind('delivery', ({ p }) => getDelivery(p)),
    // merge tax and delivery into product
    E.map(({ p, tax, delivery }) => ({ ...p, tax, delivery })) //
  );
}

请注意,即使我们使用 do,也无法转义 pipe。此外,使用 Either 将一直保持同步。如果不切换到 TaskEither 或类似的东西,您将无法获得并发。