1

I am still learning and playing with fp-ts and can't figure this out. I have some API calls and I want to collect all the successful responses and all the errors into arrays.

So, I tried to use array.sequence:

TE.map(schedules =>
  array.sequence(TE.taskEither)(
    schedules.map(({ Program, ...schedule }) =>
      pipe(
        createProgramIfNotExist(Program),
        TE.map(createdProgram =>
          setRecordingSchedules(programsClient, { ...schedule, ProgramId: createdProgram.Id }),
        ),
        TE.flatten,
      ),
    ),
  ),
),
TE.flatten

which works fine for the responses, but I only receive the last error from the API calls. Is there any way to collect all the errors into one array?

Below I wrote the functions that are making the API calls, just in case I have an issue there.

export const setRecordingSchedules = (
  fetcher: AxiosInstance,
  config: RecordingConfig,
): TE.TaskEither<Error, [ReturnType<typeof Schedule.codec.encode>, number]> => {
  const url = `/programs/${config.ProgramId}/recordingschedules`;
  return pipe(Schedule.codec.encode({ ...config, AutoPodcastConfig }), body =>
    pipe(
      TE.tryCatch(
        () => handleRateLimit(() => fetcher.put(url, body)),
        err => raiseUpdateError(unknownToError(err)),
      ),
      TE.map(({ status }) => [body, status]),
    ),
  );
};

export const createRecordingSchedule = (
  fetcher: AxiosInstance,
  program: Program.Type,
): TE.TaskEither<Error, Program.Type> =>
  pipe(
    Program.codec.encode(program),
    body =>
      pipe(
        TE.tryCatch(
          () => handleRateLimit(() => fetcher.post('/programs', body)),
          err => raiseCreateError(unknownToError(err)),
        ),
        TE.map(({ data }) =>
          pipe(
            Program.codec.decode({ ...data, Network: program.Network }),
            E.bimap(
              errors => ReportValidationError(errors, { ...data, Network: program.Network }),
              decoded => decoded,
            ),
            TE.fromEither,
          ),
        ),
      ),
    TE.flatten,
  );
Vasileios Pallas
  • 4,801
  • 4
  • 33
  • 50
  • I think, [this question](https://stackoverflow.com/questions/60471399/running-an-array-of-taskeithers-in-parallel-but-continue-if-1-or-more-task-fail) is almost the same as yours. The key is to use `array.sequence(T.task)` instead of `array.sequence(TE.taskEither)`. – ford04 Mar 20 '20 at 22:51
  • I see your point, but again, I can't concatenate all the errors into one array. – Vasileios Pallas Mar 21 '20 at 12:57

2 Answers2

0

The idea is to convert TaskEither<E, A>[] -> Task<Either<E, A>[]>, and then given a merge function Either<E, A>[] -> Either<E[], A[]> you can achieve what you want. The merge function is also done in a few steps:

  • Make a validation monoid for Either<E[], A[]>
  • Either<E, A> -> Either<E[], A[]> - trivial wrap of value or error into singleton arrays
  • foldMap Either<E, A>[] by mapping with the above wrapper and folding using above monoid
import * as E from 'fp-ts/lib/Either';
import * as A from 'fp-ts/lib/Array';

// type Error = ...
// type Result = ...

// Make a validation monoid for `Either<E[], A[]>`
const validationMonoid = E.getValidationMonoid(A.getMonoid<Error>(), A.getMonoid<Result>());

// `Either<E, A> -> Either<E[], A[]>` - trivial wrap of value or error into singleton arrays
const validationWrap = E.bimap(x => [x], x => [x]);

// `foldMap` `Either<E, A>[]` by mapping with the above wrapper and folding using above monoid
const validationMerge = E.foldMap(validationMonoid)(validationWrap);

Now you can do

const tasks: TaskEither<Error, Result>[] = ...;

const aggregatedResults: TaskEither<Error[], Result[]> = pipe(
  // convert `TaskEither<E, A>[] -> Task<Either<E, A>[]>`
  array.sequence(T.task)(tasks),
  // and then apply the merge
  T.map(validationMerge)
);
MnZrK
  • 1,330
  • 11
  • 23
  • Hey, thanks for your help. `validationWrap` returns: `Argument of type '(fa: Either) => Either' is not assignable to parameter of type '(a: Either) => Either – Vasileios Pallas Mar 22 '20 at 15:23
  • Ok, that was an issue with type inference. `array.sequence(T.task)(tasks)` returns `T.Task[]>` which is fine, but when I'm trying to use merging, an error occurs saying `Argument of type '(fa: Either>) => Either' is not assignable to parameter of type '(a: Either[]) => Either'.` Why is it trying passing a new either to the right side? – Vasileios Pallas Mar 24 '20 at 13:51
  • @VassilisPallas Not sure. I haven't run the code myself. Try to add explicit type annotations to the functions. – MnZrK Mar 24 '20 at 20:56
0

The solution was to use traverse. The idea is to get the validation task monoid and merge each E into an Array.

array.traverse(TE.getTaskValidation(A.getMonoid<E>()))(xs, TE.mapLeft(A.of))

Full working solution:

import { array, getMonoid, of as arrayOf } from 'fp-ts/lib/Array';

TE.chain(schedules =>
  array.sequence(TE.taskEither)(
    schedules.map(({ Program, ...schedule }) =>
      pipe(
        createProgramIfNotExist(Program),
        TE.map(createdProgram =>
          setRecordingSchedules(programsClient, { ...schedule, ProgramId: createdProgram.Id }),
        ),
      ),
    ),
    xs => array.traverse(TE.getTaskValidation(getMonoid<E>()))(xs, TE.mapLeft(arrayOf)),
  ),
),
Vasileios Pallas
  • 4,801
  • 4
  • 33
  • 50