An Introduction to Applicative Functors
In a previous post I talked about functors, data types which act as a context holding contents which can be mapped over. In this post I’ll talk about applicative functors, or “applicatives”, a subtype of functors for which additional functions are defined. In this post we’ll cover the core of applicatives: their interface, laws, some sample implementations, and their basic uses. This post requires familiarity with functors and currying, but doesn’t assume any other prior functional programming knowledge.
All of the examples in this post are written in JavaScript, using Flow type annotations.
Functions within contexts
Recall that functors can be thought of as providing a context holding some other type, and that for some type Foo
to be a functor this function must be defined on it:
function map<A, B>(A => B, Foo<A>) : Foo<B>
map
allows us to apply a plain function to a value which is contained in a context, or equivalently, it “lifts” a function on values to the level of a function on contexts holding values. (If this does not sound familiar, I recommend going back to my previous post on functors before continuing.) Applicative functors take this a step further by adding the functions pure
and ap
.
function pure<A>(A) : Foo<A>
function ap<A, B>(Foo<A => B>, Foo<A>) : Foo<B>
pure
places a value into a minimal context, and ap
serves the purpose of function application within a context. map
let you apply a normal function to a value inside a functor, getting a functor that held a new value in an unchanged functor context. ap
goes further; given a function and a value that are both already inside functor contexts, it combines these contexts and puts the result of the function application into the new context.
If you curry ap
you can see that it’s equivalent to these functions:
ap<A, B>(Foo<A => B>) => Foo<A> => Foo<B>
ap<A, B>(Foo<A => B>) => (Foo<A> => Foo<B>)
So another way to look at ap
is that it lets us turn a function within a context into a function that operates on contexts. Effectively ap
can let us lift a function through a functor.
We said before that applicatives are a subtype of functors. We can demonstrate that by showing that for any type where pure
and ap
are defined, we can trivially define map
as well:
function map<A, B>(fn: A => B, a: Foo<A>) : Foo<B> {
return ap(pure(fn), a);
}
The applicative laws
Just as with functors, the functions in an applicative functor’s interface need to follow a set of mathematical laws. These laws are more complicated than the functor laws; while it’s important to get a general idea of them now, you probably don’t need to sweat the details too much until you’re writing your own applicative functors. The gist of these laws is that when using ap
to apply functions to values inside applicatives, things work about the same as when we apply functions to values normally. There are four applicative laws in total:
ap(pure(id), u)
must always equalu
, whereid
is the identity functionx => x
andu
is an applicative functor. This is called the “identity law”. Here,u
is a value already in a context; the identity law tells us that applying the identity function preserves values. (Just like how applying the identity function outside of a context preserves values.) This law also shows that the context created bypure
doesn’t change the context ofu
when they’re combined usingap
.- Say we have this
compose
function:
// The definition of function composition in curried form.
compose<A, B, C>(f: B => C) : (A => B) => (A => C) {
return (g: A => B) =>
a => f(g(a));
}
// Compose lets us take a function f of type B => C, and a
// function g of type A => B, and combine them into one
// function of type A => C by feeding the result of g into f
and three values of these given types:
u : Foo<B => C>,
v : Foo<A => B>,
w : Foo<A>
Then ap(ap(ap(pure(compose), u), v), w)
must be equal to ap(u, ap(v, w))
. This is called the “composition law”, and it means that using ap
to compose functions contained within applicatives is the same as using normal function composition inside of an applicative.
3. ap(pure(f), pure(x))
must always equal pure(f(x))
. This is called the “homomorphism law”. This law means that it doesn’t matter if we apply functions to values before or after putting them into a pure context; we’ll get the same result either way.
4. ap(u, pure(x))
must always equal ap(pure(f => f(x)), u)
. This is known as the “interchange law”. Here u
is an applicative which holds some function A => B
, and x
is something of type A
. f => f(x)
is just function application to some x
, so the interchange law shows us that function application works the same whether we use ap
to apply a function to an applicative value or pure
to lift normal function application into a context.
If these all seem very abstract, that’s okay. The most important take-away while learning to use applicatives is that ap
behaves like normal function application, just inside of a functor, and that pure
produces contexts that can be combined with other contexts, without modifying the other context.
That said, once you start writing your own applicatives, these laws are very important and very valuable. The applicative laws allow us to know a lot about the behavior of any given applicative without looking at its internal implementation. As we’ll see in later in this post, it’s easy to swap out one applicative for another without changing the structure of code which operates on applicatives; if we were afraid of an applicative having some strange quirk in its implementation, we wouldn’t be able to do this.
In general, laws establish a set of ways in which an abstraction cannot leak and allow us to build abstractions which are truly generic. This is very different from polymorphism without laws, where two abstractions which implement the same interface may behave wildly and unpredictably differently from one another. So when implementing the applicative functions for some type, you should go through each of the laws and make sure they hold for your instance, because this will ensure that other developers’ intuition about your applicative’s behavior will be correct.
Simple examples
Let’s look at the applicative implementation of three types from the last post: Set
, Promise
, and Maybe
. First up, Set
:
function pureSet<A>(a: A) : Set<A> {
return new Set().add(a);
}function apSet<A, B>(fnSet: Set<A => B>, aSet: Set<A>) : Set<B> {
const result = new Set();
for (const fn of fnSet) {
for (const a of aSet) {
result.add(fn(a));
}
}
return result;
}
The pure
implementation for Set
just creates a set with a single given element in it. The ap
implementation produces a new set containing the results of applying each function from the first set to each value of the second. We can see apSet
in action:
const fns = new Set([
x => x * 2,
x => x + 1,
x => x * x]);const numbers = new Set([1, 2, 3, 4]);console.log(apSet(fns, numbers));
// Set { 2, 4, 6, 8, 3, 5, 1, 9, 16 }
Note that while we have three functions and four numbers, for a result of 12 pairings, there are only 9 values in the resulting set. It’s Set
context’s behavior that duplicate values are only saved once.
The implementation of Promise
's applicative functions is even simpler than Set
's:
function purePromise<A>(a: A) : Promise<A> {
return Promise.resolve(a);
}function apPromise<A, B>(fnPromise: Promise<A => B>,
aPromise: Promise<A>) : Promise<B> {
return fnPromise.then(fn => aPromise.then(a => fn(a)));
}
This does exactly what one might expect, applying a function in a promise to a value in a promise:
const squarePromise = purePromise(x => x * x),
threePromise = purePromise(3);apPromise(squarePromise, threePromise)
.then(x => console.log(x)); // 9
Finally let’s implement an instance for Maybe
. Recall that Maybe
gives us a safe way to express that a value might be null:
type Maybe<A> = null | Just<A>;class Just<A> {
value: A;
constructor(value) {
this.value = value;
}
}
Because Maybe
is a union type, we have more decisions to make when implementing its applicative interface. Fortunately we have guidance; there’s only one possible implementation which satisfies the applicative laws.
function pureMaybe<A>(a: A) : Maybe<A> {
return new Just(a);
}function apMaybe<A, B>(fn: Maybe<A => B>, a: Maybe<A>) : Maybe<B> {
return fn === null || a === null ?
null :
new Just(fn.value(a.value));
}
We can see this in action as well:
console.log(apMaybe(
pureMaybe(x => x + 1),
pureMaybe(3))); // Just { value: 4 }console.log(apMaybe(null, pureMaybe(3))); // null
console.log(apMaybe(pureMaybe(x => x + 1), null)); // null
console.log(apMaybe(null, null)); // null
The functor instance of Maybe
allowed us to safely apply a function to a value which might be null. Maybe
's applicative functor instance allows us to do the same when the function may be null as well.
Working with functions of multiple arguments
When looking at the signature for ap
, you may find yourself wondering why you’d end up with a function inside a functor in the first place. The trick with applicatives is that ap
usually isn’t used when you just incidentally find yourself with a functor full of functions; it’s more common purpose is to let us work with functions that we’ve lifted ourselves, using pure
.
As an example, lets say we have two sets of numbers. We want to subtract every number in the second set from every number in the first, and get a set of the answers. For subtraction we’ll use this function:
function subtract(x: number, y: number) : number {
return x - y;
}
We want to find a way to use subtract
with things of type Set<number>
, but we face a problem: map
and ap
only work with unary functions (functions that take a single argument), while subtract
is a binary function (taking two arguments). We can convert subtract
into a unary function by currying it:
const curriedSubtract = curry2(subtract);
Now curriedSubtract
has the type number => (number => number)
; it takes a number and returns a function. If we use map
to apply curriedSubtract
to a set of numbers, then we’ll get a set of functions:
const nums = new Set([5, 6, 7]);
const fns = mapSet(curriedSubtract, nums);
Here fns
has the type Set<number => number>
. Using curry2
and mapSet
, we’ve made three functions by binding each number in the input set to the first argument to subtract
. fns
is thus equivalent to a set like this:
new Set([
x => subtract(5, x)
x => subtract(6, x),
x => subtract(7, x)
])
Now that we have a set of functions, we can use ap
to apply it to a set of numbers:
console.log(apSet(fns, new Set([1, 2, 3])));
// Set { 4, 3, 2, 5, 6 }
If we put these steps in one line, we end up with this:
apSet(mapSet(curriedSubtract, new Set([5, 6, 7])),
new Set([1, 2, 3]));
In this way we’ve taken subtract
, a function whose arguments are each a plain value, and applied it to values inside contexts. In my functor posts I said that the Set
context can be thought of as representing something that can have multiple possible values. If you look at it like this, what we’ve said here is “When you take something that is either equal to 1, 2, or 3, and subtract it from something that is either equal to 5, 6, or 7, your answer will be 2, 3, 4, 5, or 6”. Here’s a slightly more complex case:
function isBetween(x: number, y: number, z: number) : boolean {
return x < y && y < z;
}apSet(apSet(mapSet(curry3(isBetween), new Set([1, 2])),
new Set([3, 4, 99])),
new Set([5, 6]));
// Set { true, false }
We can nest apSet
multiple times to work with functions of more arguments. Here we took a function which determines whether one number is between two others, and used it to find out whether any or all all numbers in one set is between those of two other sets.
Hopefully this shows a little bit of the power of applicatives: with the applicative instance of Set
, we can write programs that model non-determinism. For any function on normal values, we can “lift” that function to take sets of values, and then use these sets to compute every possible output of the function for the given sets of inputs.
Using pure
with nested applications of ap
like this is such a common task that it’s helpful to have functions to do it for us:
function liftA2Set<A, B, C>(
fn: A => B => C,
s1: Set<A>,
s2: Set<B>) : Set<C> {
return apSet(mapSet(fn, s1), s2);
}function liftA3Set<A, B, C, D>(
fn: A => B => C => D,
s1: Set<A>,
s2: Set<B>,
s3: Set<C>) : Set<D> {
return apSet(liftA2Set(fn, s1, s2), s3);
}function liftA4Set<A, B, C, D, E>(
fn: A => B => C => D => E,
s1: Set<A>,
s2: Set<B>,
s3: Set<C>,
s4: Set<D>) : Set<E> {
return apSet(liftA3Set(fn, s1, s2, s3), s4);
}// Etc. We can implement liftA for functions of any arity.
liftA
takes care of the boilerplate of nesting calls to ap
for us, and lets us directly say, “apply this function on normal values to these values in context”. As you might have guessed, liftA
stands for “lift applicative”, and the number in the function names is the arity of the function that each function lifts. liftA
makes our code much more succinct:
liftA2Set(curry2(subtract), new Set([5, 6, 7]), new Set([1, 2, 3]));liftA3Set(curry3(isBetween),
new Set([1, 2]),
new Set([3, 4]),
new Set([5, 6]));
If the name “lift” seems weird here, it might be helpful to look at the type of liftA2Set
in curried form:
(A => B => C) => Set<A> => Set<B> => Set<C>
// equivalent to
(A => B => C) => (Set<A> => Set<B> => Set<C>)
Viewing it this way, we can see that liftA
takes functions on plain values and brings them up into the world of a functor.
While the liftA
functions in this section are specific to sets, higher-kinded libraries like flow-static-land
provide the functions liftA2
and liftA3
which work on any applicative.
Composing applicatives
An important attribute of applicatives is that they compose. That is, if we combine two applicatives, the result will itself be an applicative. Let’s look at an example of composing Promises with Sets. For this example, we’ll use a function of three arguments, which takes the dimensions of a rectangular prism and returns the prism’s volume:
function prismVolume(length: number, width: number, height: number) : number {
return length * width * height;
}
Let’s say that we want to compute this volume, but each of the dimensions are inside of promises. We can use Promise
’s applicative instance to handle this case.
const length = Promise.resolve(2),
width = Promise.resolve(3),
height = Promise.resolve(4);liftA3Promise(curry3(prismVolume), length, width, height)
.then(volume => console.log(volume)); // 24
As we covered in the last section, we can do the same thing using sets:
const lengthSet = new Set([2]),
widthSet = new Set([3, 4]),
heightSet = new Set([5, 6, 7]);liftA3Set(curry3(prismVolume),
lengthSet,
widthSet,
heightSet);
// Set { 30, 36, 42, 40, 48, 56 }
If we want to deal with Promises containing Sets, though, things get a little more difficult. Our ap
functions apply a function directly to the contents of an applicative, and can’t handle the existence of a second applicative in the middle. The solution is to create a new applicative which is a combination of the Set
and Promise
applicatives, with its own ap
and pure
functions. We’ll call this new applicative SetPromise
, and we can create it in a mechanical way:
type SetPromise<A> = Promise<Set<A>>;function pureSetPromise<A>(a: A) : SetPromise<A> {
return purePromise(pureSet(a));
}function apSetPromise<A, B>(
fn: SetPromise<A => B>,
a: SetPromise<A>) : SetPromise<B> { return liftA2Promise(curry2(apSet), fn, a)
}
The implementation of pure
for a SetPromise
is easy; it’s just normal function composition of purePromise
and pureSet
. The implementation of ap
is more puzzling. liftA2Promise
lifts a function whose arguments aren’t promises to work on promise arguments; we can think of it as “peeling away” the promise from the input arguments, and then wrapping the applied function’s output in a promise. So by lifting apSet
with liftA2Promise
, apSet
gets the contents of fn
(which has type Promise<Set<A => B>>
) and a
(which has type Promise<Set<A>>
) with their promise layers stripped away. This means apSet
gets arguments of type Set<A => B>
and Set<A>
, just as it expects.
This may seem a little mind-bending, but what’s important is that this implementation is the same no matter what two applicatives we’re composing. If we’re using higher-kinded types, we can implement a composeA
function which takes the pure
and ap
arguments of two applicatives and returns the pure
and ap
functions of their composition as a result.
Using liftSetPromise
and pureSetPromise
we can implement liftA
on SetPromise
, just as we did before. And liftA
lets us accomplish our original goal, taking a function on numbers and giving it arguments which are promises of sets of numbers:
const length = Promise.resolve(new Set([2])),
width = Promise.resolve(new Set([3, 4])),
height = Promise.resolve(new Set([5, 6, 7]));liftA3SetPromise(curry3(prismVolume), length, width, height)
.then(volume => console.log(volume));
// Set { 30, 36, 42, 40, 48, 56 }
As you can see, whether the concrete structure we’re dealing with is a set, a promise, or a combination of the two, applicatives give us a standard way to apply functions to a structure’s contents.
So there’s the basics of applicative functors. By slightly expanding the signature of functors, we’ve greatly increased their capabilities: applicatives let us use normal, context-unaware functions of any arity to work with values inside of contexts. Applicatives are a perfect demonstration of core functional principles. When coding with them, ap
and pure
encapsulate the internal representation of our abstractions, while normal functions express the computations we want to perform on the contents of these abstractions. This lets calling code focus on the “what” that we want to do to these abstractions, without having to specify the “how” of how to actually tear the structures apart and put them back together. This separation of responsibilities gives us strong abstractions, because we can completely swap out one applicative for another without changing the way we apply functions to their contents. By letting us use normal functions in a wide range of contexts, applicatives can help us keep our code structured, well-factored, and composable.
Further resources
If you want to get more experience working with applicatives, it can be helpful to implement them yourself for some data types. Good candidates are Array<A>
and type Thunk<A> = () => A
. (Note that there are two different valid implementations for Array
.) Whenever you implement an applicative, don’t forget to verify that your implementation obeys the applicative laws.
If you want to make practical use of applicatives in typed code it is helpful to use a library which provides higher-kinded types, like flow-static-land, fp-ts, or funfix. Alternatively, the fantasy-land and static-land specifications give guidance for using applicatives, functors, and other algebraic structures in untyped code. Most of these use the name of
to refer to the function pure
, but the concepts are the same.
The use of applicative functors isn’t limited to simple structures like sets and promises. More complex applicatives are possible, and can be used to express certain side effects idiomatically in pure code. I’ve written a three-part series about this, which begins here. You can view a complete list of all of my posts on algebraic structures here.
The full source for all of the code in this post may be found here.
Thanks to Lizz Katsnelson for editing this post.