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 TaskEither
s into Either
s 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 TaskEither
s, 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).