2

I have a function that broadcasts an event to my subscribers and the outcome is a TaskEither<Error, void>[]. So far I've been using sequenceArray to map things like this to a TaskEither<Error, void[]> but in the current case I'd like to gather the left values, and not the right values.

I tried swap(sequenceArray(results.map(swap))) but then I realized that this doesn't even work as I end up with a void when there is an error (because left short-circuits). How can I do this mapping correctly?

Adam Arold
  • 29,285
  • 22
  • 112
  • 207

1 Answers1

1

The complexity comes in from the fact that the Task part of the TaskEither needs to run before the values can be separated. There is a helper for Either in the Array section of the library that would be useful here if this were just Either<Error, void>[].

import { separate } from 'fp-ts/lib/Array';
const broadcasts: Either<Error, void>[] = broadcastSync(); // Pretend it's not a promise
const errors: Error[] = pipe(
  broadcasts,
  separate,
  ({ left }) => left,
);

But now we come back to the Task issue. So the question is, what to do with the promise. A simple option is to have an async function that resolves all of the TaskEithers into Eithers and then to use the approach above:

import * as A from "fp-ts/lib/Array";
import * as TE from "fp-ts/lib/TaskEither";
import { pipe } from "fp-ts/lib/function";

declare function broadcasts(): Array<TaskEither<Error, void>>;

async function printErrors() {
  // This fires off all the promises and awaits them.
  const broadcasts = await Promise.all(
    pipe(
      broadcast(),
      A.map((e) => e())
    )
  );

  // Same as above
  console.log(pipe(broadcasts, A.separate, ({ left }) => left));
}

The alternative is to write your own separate that returns something like Task<Separated<A[], B[]>> that would loop through the TaskEithers, fire off the underlying promises, and then collect the results into a new promise that could be awaited.

Edit

I figured there must be a way to get this working with Compactable but I was having trouble deciphering all of the generic types when I made my original post. I've got it working, however, so here is an approach that creates separate in a more abstract way:

import * as TE from "fp-ts/lib/TaskEither";
import * as T from "fp-ts/lib/Task";
import * as A from "fp-ts/lib/ReadonlyArray";
import * as E from "fp-ts/lib/Either";
import * as C from "fp-ts/lib/Compactable";
import { pipe } from "fp-ts/lib/function";

// This is a helper to unflatten the TaskEither
function split<E, A>(either: TE.TaskEither<E, A>): T.Task<E.Either<E, A>> {
  return async () => {
    return await either();
  };
}

// Create something like `A.separate` but for `Task<Either<A,B>[]>`
const separate = C.separate(T.Functor, A.Compactable, A.Functor);

function broadcast(): TE.TaskEither<Error, void>[] {
  return [TE.left(new Error("blah"))];
}

// Here's an example of using `separate` and the other helpers
pipe(
  // Get the TaskEithers
  broadcast(),
  // Natural transform to the type expected by separate
  A.map(split),
  // Task<Either<Error, void>>[] -> Task<Either<Error, void>[]>
  T.sequenceArray,
  // Now we can use separate
  separate,
  // Finally, force the task and use the error values.
  async ({ left }) => {
    console.log(await left());
  },
); // -> [Error]

The bit that calls C.separate deserves some further explanation. That section of the library defines separation in very abstract terms. In order to get a separate function for arbitrary functors, you must pass into it type classes for the wrapper (in this case Task), a Compactable instance to inform separate how to collect up left values (in this case A.Compactable) and a functor for containing the right values (in this case again an array).

Souperman
  • 5,057
  • 1
  • 14
  • 39
  • 1
    Wow. I'll get back to you once I'm back from my vacation, but I ended up with a simpler solution I just don't have my code here yet. – Adam Arold Jul 29 '23 at 20:16