This holiday weekend has enlightened me on some important topics in functional programming. It has persuaded me to take a real look at category theory.
It all began with a need of intuition on Applicative Functors. They really are such exquisite things.
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'
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>
— usingOption<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 * -> *
such as Maybe
or List
) 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.
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 la2 * 2.
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 usen-ary
functions and apply it overn
applicatives!
To understand how powerful this is. Think about Future
s, 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.