fp-ts Как обрабатывать асинхронные операции внутри канала

#typescript #fp-ts

Вопрос:

Я изучаю fp-ts и задаюсь вопросом, как я могу лучше организовать свои функции, чтобы избежать вложенных складок. Все примеры, которые я вижу в Интернете, имеют приятный оптимизированный вызов функции канала, но я не могу понять, как избежать вложенных складок.

Некоторый контекст — На высоком уровне цель этого кода состоит в том, чтобы создать местоположение и, если это удастся, создать Станцию. Если какая-либо операция завершится неудачно, верните вызывающему абоненту соответствующую ошибку. Если все в порядке, верните 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);
  }
 

Ответ №1:

Представьте , что мы работаем с Promise тем, каким будет код? Вы свяжете в цепочку весь код обработки хороших .then случаев и прикрепите только один обработчик плохих случаев с финалом .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),
  })
}
 

Версия fp должна иметь ту же структуру, только с другим типом. Мы можем использовать TaskEither тип для модели 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()
}
 

Прилагается демо-версия игровой площадки ts с заглушками.

Игровая площадка TS

Комментарии:

1. Спасибо, это очень полезно. Я не знал об TE.throwError этом ; это полезная конструкция. Одна вещь, которую я все еще пытаюсь понять, это почему нам нужно преобразовать первую часть в функцию, которая возвращает обещание. Т. Е. почему мы не можем просто использовать вызов службы вместо createLocationTask ? В связи с этим я замечаю, что в цепочке задач нет await s… Это для того, чтобы мы могли выполнять обещания на протяжении всего процесса — и await chainedTask в конце концов?

2. Первая часть, потому что это контракт, который вам нужно подписать перед использованием pipe функции. Труба требует, чтобы все в трубопроводе было одного типа. Поскольку нисходящий обработчик ReturnType<typeof TE.fold> указывает , что он принимает аргумент типа TaskEither , который равен () => Promise<Either> , нам нужно преобразовать вызов службы в этот тип, таким образом createLocationTask .

3. Вторая часть, я думаю, вы более или менее уловили суть. Просто для ясности, вставка нескольких ожиданий не причиняет вреда, но и не помогает. Ожидание полезно только тогда, когда вы хотите «развернуть» обещание, но у нас здесь нет прецедента.