Reputation: 10887
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
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