(Re)Encounter with Applicative Functors

This holiday weekend has genuinely (finally) enlightened me on some important topics in functional programming. It has persuaded me to take a real look at category theory. Indeed, I am very excited about what's next in my endeavors.

It all began with the intuition of Applicative Functors. They really are such exquisite things.

I am by no means authoritative on the topic, my study has inspired me to try and write about it in a way that is hopefully understandable.

Alright, prologue aside, let's dive in. First off, let's quickly revisit Functor, HKTs, their implications and how they're important. The language I will be using is Haskell (for now), for obvious reasons, but will try to explain it well.

Behind free monads, applicatives may be #1 pattern in FP IMO

— close friend

Revisiting Functors ↔

A Functor is defined over types of kind * -> *, or of unary type constructors. You've probably come across them quite a bit, as they include List or [] and Maybe. Where types of kind * are what we call concrete types (like Int).

As an informal side note. In other languages they are quite literally similar to generic types such as List<T>, Option<T> — using Option<f64>, for example, in Rust. What we really want is higher-kinded polymorphism.

Let's take a look at the Functor typeclass (I'll explain)

class Functor f where
    fmap :: (a -> b) -> f a -> f b

If you're unfamiliar with haskell syntax, that's okay. This essentially means for all instances of Functor it must implement a function fmap that takes a function of type (a -> b) and a f a (where f is the type constructor or 'context': that is a type of kind * -> *) and returns a f b.

The best way to understand this concretely for someone just starting is to take a look at List or []. Remember map? Its signature looks like:

map :: (a -> b) -> [a] -> [b]

Did something just click there?!? What about the Functor instance for List?

instance Functor [] where  
    fmap = map  

I'm sure most of you have mapped over lists. But for completeness, An example is like map (*2) [1..3] which gives us [2,4,6]

That's right! Indeed List is a Functor, but there are in fact many types which are Functor! We won't go over that here. The point is functors are things that can be mapped over.

Again, for completeness, Functors (Applicatives, and Monads) implementations must obey laws. I will omit proofs or anything rigorous here but be weary the instances should abide by the laws.

The Applicative Meat 🍖

Time for the juicy insides. Let's take a look at the definition:

class (Functor f) => Applicative f where
    pure  :: a -> f a
    (<*>) :: f (a -> b) -> f a -> f b

From our Functor recap, some things here should be immediately clear. Also, for one, Applicatives are indeed themselves also Functors. Cool. So we see Applicatives implement a pure function and a (<*>) (an infix function here, it's also called apply in other languages).

Is it obvious what pure does here? It should be! if f is some functorial context, then a -> f a lifts a into the Applicative Functor.

What about (<*>)? It looks quite similar to our fmap above, doesn't it? Except we notice the f (a -> b). Hmm. So it takes a Functor that has a function in it and applies that function to a second Functor. If you 'erase' the f from the type signature, you get normal function application. Let's take a look at examples.

img

Holy Moly! See what's happening here? For one, In the first example [ (2 *) ] is already a function in a List (List is also an Applicative Functor!). So it's applying the 2 * function into [5]

In the second example, we lift the binary (*) function into the applicative context and then apply it to our applicatives.

So what's the big deal? 🤔

First off, suppose <$> is the infix version of fmap. What if we wanted to map a function over a collection of Applicative Functors?

Note that (*) is a binary infix function that takes two parameters a la 2 * 2.

img

Wait one second here... You're telling me I can take a function and fmap it over a collection of Applicative Functors? Inside their context?

Note that we only use (*) here which takes 2 parameters. The real magic here is that we could use n-ary functions and apply it over n applicatives!

To understand how powerful this is. Think about Futures, Either or Maybe and other very useful types. Suppose you have defined a Router or Middleware over a collection of routes that is an Applicative Functor. How incredible and effective it would be to fmap a function over and determine or compose independent effects for all routes.

When I realized how powerful this truly is, I was moved to order a category theory book straight away. Monads do have their shine, and for good reason! But to touch briefly on the difference, they are dependent on the value and context a -> f b. Applicatives are less restrictive, and have many uses within all functorial types f a.

Until next time.

— Jahan

Show Comments