...

/

Monads for Asynchronous Behavior

Monads for Asynchronous Behavior

Learn about combining Task and Either to handle asynchronous Tasks.

TaskEither vs. Task

TaskEither is a monad provided by fp-ts. It’s actually a monad Transformer. We already mentioned how we can stack monads on top of each other to combine their effects. That helps us understand why Either reference is there. What about Task? We haven’t seen that one before!

Task is a monad for asynchronous non-failing calls. A Promise is easily converted into a Task. Simply create a function that returns a Promise. That’s it!

Press + to interact
const thisIsATask: T.Task<number> = () => Promise.resolve(42);

This monad is useful because it’s a monad. We can perform all kinds of transformations, like with any other monad:

Press + to interact
// result is still T.Task<number>
const result = pipe(
thisIsATask,
T.map(n => n + 1) // after getting back the result, we increase it by 1
);

Similarly, we can use chain to combine multiple Tasks. That means we now have the power of monads along with their composability and ease of use for the common asynchronous Node functionality. Eventually, when we actually need the results, we call the Task as a function and it returns the Promise it contains.

Press + to interact
// now result is Promise<number>
const result = pipe(
asTask,
T.map(n => n + 1)
)();

Before we continue, let’s take a small detour. As we mentioned several times before, functional programming tries to keep everything pure and thus easy to reason with. Side effects and I/O ruin this condition because they add impurity to our program. If our function uses I/O, it gives different results, despite having the same input. For instance, if we call the Lambda we’re monitoring more frequently, we get back a different sum. This is why FP does the best it can and moves all this content to the edge. So, despite what we might be expecting, this is pure because nothing is actually happening. There’s no asynchronous behavior (that is, a Promise) being run.

Press + to interact
const thisIsATask: T.Task<number> = () => Promise.resolve(42);
const result = pipe(
thisIsATask,
T.map(n => n + 1)
);

We’re now creating a Task and telling our code to perform a map on it when it’s run. It doesn’t start running, though. We can test this by adding a console.log statement. Nothing prints.

Press + to interact
const asTask: T.Task<number> = () => {
return new Promise((resolve) => {
console.log('Doing stuff')
return resolve(42);
});
};
const result = pipe(
thisIsATask,
T.map(n => n + 1)
);

At some point, we’ll actually have to run the Task. Otherwise, why did we create it? That said, FP leaves this ...