11

In fp-ts, I'm trying to chain some potentially failing async tasks together with TaskEither but I need to use the results from intermediate tasks later on down the chain.

In this example:

const getFoo = (a: string): Promise<Foo> => {};
const getBar = (foo: Foo): Promise<Bar> => {};
const mkFooBar = (foo: Foo, bar: Bar): Promise<FooBar> => {};

const async main1: Promise<FooBar> => {
  const a = "a";
  const foo = await getFoo(a);
  const bar = await getBar(foo);
  const fooBar = await mkFooBar(foo, bar);

  return Promise.resolve(fooBar);
};

const main2: Promise<FooBar> => {
  const a = "a";

  return pipe(
    TE.tryCatch(() => getFoo(a), e => e),
    TE.chain(foo => TE.tryCatch(() => getBar(foo), e => e)),
    TE.chain(bar => TE.tryCatch(() => mkFooBar(??, bar), e => e))
  );
};

the main1 function is an async/await-style solution to this problem. What I'm trying to do is emulate something like this in a fp-ts chain-style. main2 is my attempt at this.

Because the async/await version introduces all the intermediate results into the local scope (i.e. foo and bar), it's easy to call mkFooBar which depends on both those results.

But in the fp-ts version, the intermediate results are trapped in the scope of each task.

The only way I can think to make this version work would be to make either the async functions themselves (i.e. getFoo and getBar) also return their arguments, or perhaps the TaskEither wrappers return the arguments so that they can then be passed on to the next function in the chain.

Would this be the correct way to do this? Or is there a simpler version which more closely resembles the async/await version?

ironchicken
  • 754
  • 6
  • 19

1 Answers1

19

Depending on how many times you'll need to access the intermediate results in the following computation, I would suggest either using Do (an approximation of Haskell's do notation), or carry the intermediate results over via manual mapping.

Given:

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

declare function getFoo(a: string): TE.TaskEither<unknown, Foo>;
declare function getBar(foo: Foo): TE.TaskEither<unknown, Bar>;
declare function mkFooBar(foo: Foo, bar: Bar): TE.TaskEither<unknown, FooBar>;

Example with Do:

function main2(): TE.TaskEither<unknown, FooBar> {
  return pipe(
    TE.Do,
    TE.bind("foo", () => getFoo("a")),
    TE.bind("bar", ({ foo }) => getBar(foo)),
    TE.chain(({ foo, bar }) => mkFooBar(foo, bar))
  );
}

Example mapping manually:

function main3(): TE.TaskEither<unknown, FooBar> {
  return pipe(
    getFoo("a"),
    TE.chain(foo =>
      pipe(
        getBar(foo),
        TE.map(bar => ({ foo, bar }))
      )
    ),
    TE.chain(({ foo, bar }) => mkFooBar(foo, bar))
  );
}
Giovanni Gonzaga
  • 1,185
  • 9
  • 8
  • This solution should be included as example in fp-ts documentation. Chaining tasks through pipes is not easy at first when learning fp-ts, and this answer covers many basic usages. – zenbeni Dec 16 '21 at 10:11
  • @zenbeni have you seen https://gcanti.github.io/fp-ts/guides/do-notation.html? It should cover Do notation already. I also just updated the answer to use Do from fp-ts instead of from fp-ts-contrib, available in newer versions – Giovanni Gonzaga Dec 17 '21 at 10:37