2

I'm learning fp-ts and am wondering how can I better organize my functions to avoid nested folds. All of the examples I see online have a nice streamlined pipe function invocation, but I can't figure out how to avoid the nested folds.

Some context - At a high level, the intent of this code is to create a Location and if that succeeds, create a Station. If either operation fails, return back an appropriate error to the caller. If all is well, return a 201.

public async initialize(
    @requestParam('site') site: string,
    @request() req: Request,
    @response() res: Response
  ) {
    //use the same value for now
    const nameAndPublicId = LocationService.retailOnlineLocationName(site);
    
    const location: E.Either<ApiError, LocationDTO> = await this.locationService.createLocation(
      site,
      nameAndPublicId,
      nameAndPublicId
    );

    const stationName: string = StationService.retailOnlineStationName(site);

    pipe(
      location,
      E.fold(
        (err: ApiError) => ConfigController.respondWithError(err, res),
        async (loc: LocationDTO) => {
          pipe(
            await this.stationService.createStation(site, stationName, loc.id),
            E.fold(
              (err: ApiError) => ConfigController.respondWithError(err, res),
              (_: StationDTO) => res.status(201).send()
            )
          );
        }
      )
    );
  }

  static respondWithError(err: ApiError, res: Response) {
    res.status(err.statusCode).json(err);
  }
mattmar10
  • 515
  • 1
  • 4
  • 16

1 Answers1

3

Imagine we're working with Promise, what would the code be like? You'll be chaining all the good-case handling code with .then, and only attach one bad-case handler with a final .catch.

public async initialize(
  @requestParam('site') site: string,
  @request() req: Request,
  @response() res: Response
) {
  const stationName: string = StationService.retailOnlineStationName(site);

  const nameAndPublicId = LocationService.retailOnlineLocationName(site);
  
  // for illustration purpose, we suppose here
  // the service returns a Promise of actual value
  // instead of Promise of Either
  await this.locationService.createLocation(
    site,
    nameAndPublicId,
    nameAndPublicId
  ).then((loc: LocationDTO) => {
    return this.stationService.createStation(site, stationName, loc.id)
  }).then((_: StationDTO) => {
    res.status(201).send()
  }).catch(err => {
    ConfigController.respondWithError(err, res),
  })
}

The fp version should bear the same structure, just with a different type. We can use TaskEither type to model a Promise.

public async initialize(
  @requestParam('site') site: string,
  @request() req: Request,
  @response() res: Response
) {
  const stationName: string = StationService.retailOnlineStationName(site);

  const nameAndPublicId = LocationService.retailOnlineLocationName(site);
  
  // here the service shall return Promise of Either
  const createLocationTask = () => this.locationService.createLocation(
    site,
    nameAndPublicId,
    nameAndPublicId
  )

  const chainedTask = pipe(
    createLocationTask,
    TE.fold(
      TE.throwError, // pass through error
      (loc: LocationDTO) => async () => stationService.createStation(site, stationName, loc.id),
    ),
    TE.fold(
      // catch error
      (err: ApiError) => async () => ConfigController.respondWithError(err, res),
      (_: StationDTO) => async () => { res.status(201).send() },
    )
  )

  await chainedTask()
}

Attached is a ts playground demo with stubs.

TS Playground

hackape
  • 18,643
  • 2
  • 29
  • 57
  • Thank you, this is quite helpful. I didn't know about `TE.throwError`; thats a useful construct. One thing I'm still trying to wrap my head around, is why do we need to transform the first part into a function that returns a promise. I.e., why can't we just use the service call instead of the `createLocationTask` ? Related, I notice there are no `await`s in the chainedTask... Is this so we can deal with promises the whole way through the pipeline - and `await` the entire `chainedTask` at the end? – mattmar10 Aug 31 '21 at 13:47
  • First part, because that’s the contract you need to sign before using the `pipe` function. Pipe requires that everything in the pipeline is of same type. Since downstream handler `ReturnType` designates that it takes argument of type `TaskEither` which equals `() => Promise`, we need to convert the service call into this type, thus the `createLocationTask`. – hackape Aug 31 '21 at 14:51
  • Second part, I think you’ve got the point more or less. Just to clear, inserting a few await doesn’t do harm, but doesn’t help either. Await is only useful when you want to "unwrap" a promise, but we don’t have use case here. – hackape Aug 31 '21 at 14:58