Applicative Programming in Practice
How to use applicative functors to sequence effects (in Haskell and Typescript)
Published 2022-09-11 by Olivia MackintoshIn this article, I will open by explaining why applicative functors are useful in the real world. Then I’ll walk through solving a toy example with an applicative and `sequenceT`. And finally, I will explain a little more about the theory. In the end, you should be able to use applicative functors in your own work and hopefully have the desire to learn more in your own time.
Introduction
Sometimes we want our code to do multiple things, either in parallel, or one after another in quick succession. If one or more errors occur along the way, we may want to return all the errors instead of failing fast. Otherwise, we probably want to return some kind of result. Applicative functors can be used to achieve this, without having to express, and handle, the result of each computation in an imperative style.
Of course, I should point out that applicative functors are not limited to this application alone. As with most concepts in functional programming, they are rather generic. But being practically minded, I thought I would focus on a specific use-case here: sequencing effects that may fail and collecting the results.
A Toy Problem
Suppose we have three APIs that fetch representations of the following fruits: Apple (a), Banana (b), and Cantaloupe Melon (c). And that each of the APIs may fail to fetch these different fruits. If they were physical fruits we might want to make a fruit salad but only if they are all available. If one is missing then we cannot make the fruit salad and want to return the errors explaining why it was not possible.
If we were to do this in an imperative style, we may check the result of each request using an if statement, then throw an exception or return immediately. However, this can often result in a lot of verbosity and doesn’t easily allow for the returning of multiple errors without resorting to a mutable variable, perhaps an array that we can push errors to. This method can also hard to compose with other effects.
If we have a standard type representing success or failure, it is easier to compose and abstract.
The Either Type
A useful type commonly found within FP is the `Either` type. It can either have a “Left” value or a “Right” value, and the “Right” value is by convention a result, and the “Left”, an error.
In Haskell it has a simple definition:
data Either a b = Left a | Right b
and Typescript is not far behind:
interface Left<E> = {
: "Left"
_tag: E
left
}interface Right<E> = {
: "Right"
_tag: E
right
}type Either<E, A> = Left<E> | Right<A>
In Haskell, we may have the following API functions:
getApple :: IO (Either String Apple)
getBanana :: IO (Either String Banana)
getCantaloupe :: IO (Either String Cantaloupe)
And in TypeScript, using the fp-ts library, we may have the following:
interface FruitsAPI = {
: TaskEither<string, Apple>;
getApple: TaskEither<string, Banana>;
getBanana: TaskEither<string, Cantaloupe>;
getCantaloupe }
Notice that the type of `TaskEither<string, Apple>` is `() => Promise<Either<string, Apple>`. This means that TaskEither is a thunk. We are delaying executing the action until we need to.
I’m assuming you will have methods that will return `Promise<A | undefined>` where A is some arbitrary type. Since `TaskEither` is based on Promise, we can wrap the API and return a suitable error `String`
const getApple: TaskEither<string, Apple> = () => async () => {
const apple = await fruitsAPI.getApple();
return apple ? right(apple) : left("No apples available")
}
In Haskell, the IO and Either types are first class, so your API will probably be based on IO already and so the wrapping isn’t needed. Haskell is also lazy by default.
The next step is to sequence the actions:
const results: TaskEither<string[], [Apple, Banana, Canaloupe]> = sequenceT(ApplicativePar)(getApple, getBanana, getCantaloupe)
`sequenceT` will return either a list of our errors or