IO

The last thing functional programming can tolerate are side effects, but side effects are essential to a useful program. So, in functional programming, side effects are minimized and clustered together. IO is used to represent an operation that has side effects. See the IO definition in FP-TS:

interface IO<A> {
  (): A
}
Copy the code

You’ll see that this is just a normal function. In fact, that’s how you can hide scary side effects in pure’s world. Once a piece of code with side effects is put into a function, as long as we don’t execute the function, the evil side effects are locked in Pandora’s box and the world remains pure and beautiful. When everything is ready, we can execute this function in a corner isolated from the rest of pure code, releasing side effects 😈. Better still, IO also has Functor, Monad, and other typeclass instances.

const getRoot: IO<HTMLElement> = () = > document.documentElement;

const calcWidth = (elm: HTMLElement) = > elm.clientWidth;

const log =
  <A>(a: A): IO<void> = >() = >
    console.log(a);

const main: IO<void> = pipe(
  Do,
  chain(() = > getRoot),
  map(calcWidth),
  chain(log)
);

// impure
main();
Copy the code

Task

IO gives us the ability to use synchronous side effects in pure, while Task gives us the ability to use asynchronous side effects. The definition of Task is still simple:

interface Task<A> {
  (): Promise<A>
}
Copy the code

It turns out to be a function that returns a Promise. Similarly, tasks also have Functor, Monad, and other typeclass instances, so familiar functions like map and chain can come in handy again. So far in our discussion we have avoided the fact that both IO and Task can fail. For example, when we initiate a resource request, it is inevitable that the network is unreachable or the resource does not exist. Because of this, fP-TS provides two additional modules, IOEither and TaskEither. But they’re concepts that we’re already familiar with:

interface IOEither<E, A> extends IO<Either<E, A>> {}

interface TaskEither<E, A> extends Task<Either<E, A>> {}
Copy the code

A TaskEither a was a Task (Either e a). The advantage of using TaskEither A, compared with A Promise, is that it is harder for A user to ignore the fact that the action may fail. TaskEither provides A handy function that helps us get A TaskEither A from A Promise that is secretive about possible errors:

const tryCatch = <E, A>(f: Lazy<Promise<A>>, onRejected: (reason: unknown) => E): TaskEither<E, A> => () =>
  f().then(E.right, (reason) => E.left(onRejected(reason)))
Copy the code

For front-end development, where asynchronous operations are common, TaskEither will be used frequently.