Making Promises Functional

Hey folks, this is a quick overview on how to make promises functional in fp-ts. If you’re new to fp-ts would highly recommend reading the fp-ts guide to get more familiar with the library. Before we delve into why fp-ts, and why promises are a pain to get port onto fp-ts, wanted to first walk through why functional paradigm is a good option in the first place.

There is a heavy debate between Functional and OOP for decades with functional being more in vogue lately, but there are true benefits from having well tested and pure functional code even in the most monolithic of applications.

alt text for large image

Either you click with functional paradigm or you learn to love it, or wage a holy war on all of the online forums. I generally feel having code without side effects, self contained state and good testablility (ability to write good and easily reproduce tests without a whole lot of mocking) is a good thing. Whether you prefer functional or OOP, I think it’s entirely up to you on which to embrace or have a good balance of both if you’re language of choice allows it. Functional programming makes it easier to translate mathematical functions in atomic units v/s in OOP style where more code if often used to describe models and how they interact with each other.

That being said, if you want to add a little spice to you typescript, fp-ts is a great lib to introduce Haskell-like functional programming to your typescript code.

Promises

Great now that you’re curious on why fp-ts and know a bit about how it works, let’s dive into why promises can be finnicky to get right in fp-ts. The problem isn’t with fp-ts or promises, it’s in how they’re represented in fp-ts v/s JavaScript or TypeScript.

Let’s say you have a function to add some delay using a promise.

const delay = (ms: number): Promise<void> => {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
};

This works fine in Node or the browser, but you can’t directly use a promise in fp-ts as a Task (You can, but would need some gimmicky way of setting up a new Task to call a promise). A Task is a function that returns a promise which is expected to never be rejected.

interface Task<A> {
  (): Promise<A>;
}

Which is great but introduces a new problem if we want to chain it with Eithers or TaskEithers. Eithers represent a synchronous operation that can success or fail, like writing some text to a file or reading a value from a JSON object. TaskEithers are async versions of Eithers meaning the async operation can either succeed or fail. This is the most common use case like writing records to a database, reading from cache or making an API request. The problem arises when trying to chain a guaranteed async promises with other impure versions like Eithers or TaskEithers.

In order to do this, we need to convert the promise or Task to the base operation type you’re using in fp-ts. So you would need to convert a Promise to a Task then to a TaskEither or ReaderTaskEither to make it all flow correctly.

const delayTE = (duration: number): TaskEither<Error, void> => {
  return TE.tryCatch(
    () => delay(duration),
    (e) => new Error("Should never get called")
  );
};
const delayRTE =
  (duration: number): ReaderTaskEither<Record<string, string>, Error, void> =>
  (context: Record<string, string>) =>
    delayTE(duration);

Doing so does add some impurity to the once pure Task, but it’s not any less impure than it once was, this is pretty common in Functional languages to call pure functions from impure ones, and one of the main gotchas for me when trying to write functional code. ReaderTaskEither is a special use case for TaskEithers in which a context can be passed to other chained operations making it easier to share the new state with other functions. (You can also do this with TaskEithers but would involve having a separate argument for each part of state you need.)

Great once, we have all of this together now let’s using our delay function in a fp-ts call.

/* This function will finish after 10 seconds */
const asyncOperation = (): ReaderTaskEither<
  Record<string, string>,
  Error,
  void
> => {
  return pipe(
    sequenceT(RTE.readerTaskEither)([
      delayRTE(1000),
      delayRTE(2000),
      delayRTE(3000),
      delayRTE(4000),
    ])
  );
};

Note: how we represent delays as a ReaderTaskEither, this might make you think that it can throw an Error, but it’s actually won’t ever throw an error and will wait until the initial promise is completed.

Another interesting part of fp-ts is that pipe let’s you descibe how the data should be fetched/modified and finally prepared, but these functions are not run sequentially, they are run in async if possible. In order to partially make it synchronous, we can use SequenceT to chain them in the order we want them to run. So the final async operation will finish after 10 seconds.

Awesome, now if you want to convert an ReaderTaskEither back to a Promise and make a call you would need to setup a pipe to chain and prepare your function call.

const fn = () => {
  return pipe(
    sequenceT(RTE.readerTaskEither)([
      delayRTE(1000),
      delayRTE(2000),
      delayRTE(3000),
      delayRTE(4000),
    ]),
    /* This could be chained, folded or mapped to get a final value in the promise */
    RTE.map(() => console.log("Completed!"))
  )({ context: "some value" })();
};

/* Logs "Completed!" after 10 seconds */
fn();

Sweet, hopefully this helps in understanding how fp-ts works and how to convert native Promises to fp-ts types. Hope you enjoyed it and want to give fp-ts a spin and start using it for your core features to be well composed and tested.

Join the email list and get notified about new content

Be the first to receive latest content with the ability to opt-out at anytime.
We promise to not spam your inbox or share your email with any third parties.

The email you entered is not valid.