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?
This post will introduce two concepts in fp-ts: asynchronous tasks and error handling. Namely we will look at the Task, Either, and TaskEither types. Every asynchronous operation in modern Typescript is done using a Promise object. A task is a function that returns a promise which is expected to never be rejected.
An asynchronous call. Something that can go wrong. In functional programming we use certain data types to deal with these types of complexity. In our case specifically: Reader - for dependency injection. Task - for asynchronous things. Either - for things that can go wrong.
A task is a function that returns a promise which is expected to never be rejected. The type definition for task can be found below. Another way to define task is using a function type definition. Tasks are expected to always succeed but can fail when an error occurs outside our expectations.
Every asynchronous operation in modern Typescript is done using a Promise object. A task is a function that returns a promise which is expected to never be rejected. The type definition for task can be found below. Another way to define task is using a function type definition.
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 map
ping.
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))
);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With