0

Say I have three entities a parent entity, representing ingestion of an array of arrays over a third party API, a Child Entity representing the parsing and processing of one such array members, and errors representing the failure to process alongside an error message.

Let's say I have the following types:

export type CreateChildEntity = () => TaskEither<ServiceError, void>;

export type CheckParentMappable = (
  id: UUID
) => TaskEither<ServiceError, Option<UUID>>;

export type GetHydratedParentForMapping = (
  id: UUID
) => TaskEither<ServiceError, HydratedType>;

export type MarkParentMapped = (id: UUID) => TaskEither<ServiceError, void>;

export type MarkParentFailed = (id: UUID) => TaskEither<ServiceError, void>;

export type LookupDetails = (
  id: UUID,
  firstName: string,
  lastName: string
) => TaskEither<ServiceError, Option<ValidatedDetails>>;

export type TrackParentMappingError = (
  id: UUID,
  author: ValidatedAuthor,
  message: string
) => TaskEither<ServiceError, void>;

export type Deps = Readonly<{
  createChildEntity: CreateChildEntity;
  checkParentMappable: CheckParentMappable;
  getHydratedParentForMapping: GetHydratedParentForMapping;
  markParentMapped: MarkParentMapped;
  markParentFailed: MarkParentFailed;
  lookupDetails: LookupDetails;
  trackParentMappingError: TrackParentMappingError;
}>;

And then the following function:

import { Ctx, Deps, Input, Error } from "./types";
import { pipe } from "fp-ts/lib/pipeable";
import * as TE from "fp-ts/lib/TaskEither";
import { isSome } from "fp-ts/lib/Option";

export default (input: Input): Ctx<void> => mapParent(input);

const mapParent = (input: Input): Ctx<void> => (
  deps: Deps
): TE.TaskEither<Error, void> =>
  pipe(
    deps.getHydratedParentForMapping(input.id),
    TE.chain(hydratedParent =>
      pipe(
        TE.sequenceArray(
          hydratedParent.authors.map(a => {
            return pipe(
              deps.lookupDetails(hydratedParent.id, a.firstName, a.lastName),
              TE.chain(detail => {
                if (isSome(detail)) {
                  return deps.createChildEntity();
                } else {
                  return pipe(
                    deps.trackParentMappingError(
                      hydratedParent.id,
                      a,
                      "MISSING_DETAILS_ERROR"
                    ),
                    TE.chain(_ => deps.markParentFailed(hydratedParent.id))
                  );
                }
              })
            );
          })
        ),
        TE.chain(_ => deps.markParentMapped(hydratedParent.id))
      )
    )
  );

Full code here: https://stackblitz.com/edit/typescript-3qv4t1?file=index-bad.ts

As long as I have validation rules inside markParentFailed and markParentMapped to ascertain that the parent does (or does not) have children/errors attached, this code works.

What I would like to achieve instead is to have the initial map of lookupDetails across hydratedParent.authors to return an Either, such that I can then completely partition the flow based on the existence of failures. In effect, something like so:

const constructEither = (hydratedParent: HydratedType): Ctx<void> => (deps: Deps): TE.TaskEither<Error, Either<ReadonlyArray<Readonly<{id: UUID, author: ValidatedAuthor, errorMessage: string}>>, ReadonlyArray<ValidatedDetails>>> => {
  return TE.sequenceArray(
    hydratedParent.authors.map(a => {
      return pipe(
        deps.lookupDetails(hydratedParent.id, a.firstName, a.lastName),
        TE.chain(val => {
          if(isSome(val)) {
            return right(val.value)
          } else {
            return left({id: hydratedParent.id, author: a, errorMessage: "MISSING_DETAILS_ERROR"})
          }
        })
      )
    }))
}

const mapParent = (input: Input): Ctx<void> => (
  deps: Deps
): TE.TaskEither<Error, void> =>
  pipe(
    deps.getHydratedParentForMapping(input.id),
    TE.chain(hydratedParent =>
    pipe(
      constructEither(deps)(hydratedParent),
      TE.chain(res => {
        if(isRight(res)) {
          pipe(
            res.map(r => deps.createChildEntity()),
            ,TE.chain(_ => deps.markParentMapped(hydratedParent.id))
          )
        } else {
          pipe(
            res.map(r => deps.trackParentMappingError(
              r.left.id,
              r.left.author,
              r.left.errorMessage
            )),
            TE.chain(_ => deps.markParentFailed(hydratedParent.id))
          )
        }
      })
    )
    )
  );

This fails to compile.

Additionally, it is my understanding that even if I get it to compile, TaskEither will ignore the left branch anyway, and, effectively, throw it as an Exception.

What is the fp-ts incantation for partitioning a TaskEither in this way?

Abraham P
  • 15,029
  • 13
  • 58
  • 126
  • I'm having trouble understanding what you are trying to achieve. Can you distill the problem into something smaller? – chautelly Dec 14 '20 at 14:36
  • Thanks for following up! I'll take a stab at a further simplification tonight (the above is already a simplification). In a nutshell, I want to be able to map through a list of entities with a TaskEither operation, and store the failures in another nested taskeither. A diagram of desired behavior is here: https://drive.google.com/file/d/1M-mXYC7crM5BiHF0j4PAT7GBC4f9u448/view?usp=sharing – Abraham P Dec 14 '20 at 20:12
  • 2
    Speaking as someone who jumped in here to help as well, having so many nested pipes is usually an code smell. It makes it really hard to read what you're doing. Also, for the record, you have a `TE.chain(x => ...)` where x is an `Either` and then you test `isRight`. You'd be better served doing `TE.chainEitherK(x => ...)` where x will be the wrapped value of a Right. No `isRight` necessary. Or you could preface `TE.chain` with `TE.fromEither` and then you'd have access to `TE.chain` (for the right case) and `TE.orElse` (or `TE.mapLeft`) for the left case – user1713450 Dec 17 '20 at 07:17

0 Answers0