2

What is the best way to handle Promises of Eithers when the Right side of the Eithers do not align nicely? In this scenario, I have three non-dependent, "prerequisite" operations represented as Eithers (with different right hand types). If they all succeed, I wan't to proceed with the fourth operation. If any of the three fails, I do not wan't to proceed with the fourth operation.

At this point, I have a solution compiling, but am not happy with readability. Surely there is a more elegant way to handle the multiple Eithers in this type of scenario?

    //promise of Either<ApiError, CustomerDTO>
    const customer = this.customerService.createCustomer(siteOrigin, createCustReq);

    //promise of Either<ApiError, LocationDTO>
    const location = this.locationService.getRetailOnlineLocation(siteOrigin);

    //promise of Either<ApiError, StationDTO>
    const station = this.stationService.getRetailOnlineStation(siteOrigin);
    
    //execute previous concurrently
    const locationAndStationAndCustomer = await Promise.all([location, station, customer]);

    const locationE = locationAndStationAndCustomer[0];
    const stationE = locationAndStationAndCustomer[1];
    const customerE = locationAndStationAndCustomer[2];


    //How to make this better?
    const stationAndLocationAndCustomer = E.fold(
      (apiErr: ApiError) => E.left(apiErr),
      (location: LocationDTO) => {
        return E.fold(
          (apiErr: ApiError) => E.left(apiErr),
          (station: StationDTO) =>
            E.right(
              E.fold(
                (err: ApiError) => E.left(err),
                (customer: CustomerDTO) =>
                  E.right({ location, station, customer })
              )(customerE)
            )
        )(stationE);
      }
    )(locationE);
mattmar10
  • 515
  • 1
  • 4
  • 16
  • Not familiar with fp-ts, but normal use of the Promise API would be to `reject`, not `resolve`, a `Promise` in the event of an error. If you did that, you could simply write `Promise.all([location, station, customer]).then([location, station, customer] => doNextStep(location, station, customer));` and the Promise API would automatically take care of only proceeding if all 3 prerequisites succeed. – meriton Oct 05 '21 at 21:46
  • After a little more digging, I found `sequenceT`. Is this an appropriate use of `sequenceT`? `const sequenced = sequenceT(E.either)(locationE, stationE, customerE);` and a related question - `E.either` is deprecated. I see this message "Use small, specific instances instead." But, I'm not sure what that means or how to replace it. – mattmar10 Oct 06 '21 at 01:06
  • `E.Apply` is an instance of `Applicative` alone for `Either` which is all that is needed to get a `sequence` function. `E.either` contained a lot of other methods that were mostly not needed. I think it was deprecated to help with tree shaking, etc. – Souperman Oct 10 '21 at 07:26

1 Answers1

1

I think the comments were getting close to the right answer. sequenceT is a correct approach to this type of problem.

import { sequenceT } from 'fp-ts/Apply'
import * as E from 'fp-ts/Either';

const seq = sequenceT(E.Apply);

return pipe(
  await Promise.all([location, station, customer]),
  seq, // [Either<...>, Either<...>, Either<...>] => Either<ApiError, [...]>
  // map is a bit less cumbersome. If the value was Left it returns Left
  // otherwise it calls the function which returns a new Right value from
  // what the function returns
  E.map(([loc, sta, cust]) => ({
    location: loc,
    station: sta,
    customer: cust,
  })), 
);
Souperman
  • 5,057
  • 1
  • 14
  • 39