Kevin Le - Khnle
Kevin Le - Khnle

Reputation: 10887

Branch and merge in fp-ts

I have the following 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}`
      )
    }
  )
)

where

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

Step 1 produces output { count: 10, pricePerItem: 5 }

Step 2 takes the output of step 1 as input and produces output { count: 10, pricePerItem: 5, tax: 0.1 }

Step 3 takes the output of step 2 which is { count: 10, pricePerItem: 5, tax: 0.1 } and produces output { count: 10, pricePerItem: 5, tax: 0.1, delivery: 0.15 }

Step 4 is just a placeholder where it could potentially produces a left to indicate an error condition. I just left it out.

It works as expected in a pipe. But I do NOT want that.

I want step 2 to takes the input { count: 10, pricePerItem: 5 } and add tax to it. Parallelly, I want step 3 to take the same input { count: 10, pricePerItem: 5 } and add delivery to it.

Then I want step 4 to take the output of step 2 and step 3 and merge them back.

I saw something involved bind and/or do notation such as in this answer but is not quite sure.

So how the flow be branched and merged as opposed to always run in a pipe?

Update The equivalent in imperative programming is as follow:

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

const productsWithTaxAndDelivery = getWithTaxAndDelivery(productsWithTax, productsWithDelivery)

The point is to I do not want the true pipe.

Upvotes: 1

Views: 415

Answers (1)

jmartinezmaes
jmartinezmaes

Reputation: 371

You can definitely use do notation here. Here's a solution that stays true to what you're looking for:

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 }))
  );
}

Admittedly, there's some unnecessary overlap of return types here and we have to awkwardly merge the object at the end. Instead of returning extended objects between functions, you could return tax and delivery as standalone results and merge everything at the end:

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 })) //
  );
}

And note, even though we're using do, there's no escaping pipe. Also, working with Either is going to be synchronous all the way. You won't be able to get concurrency without switching to TaskEither or something similar.

Upvotes: 2

Related Questions