Functors, Applicatives, and Monads
A bit of category theory and types systems knowledge here.
Functors
In haskell, functors are basically type constructors that
define the fmap function, i.e., they take a type
t and wrap them in their own type which is constructed
by passing t to the type constructor.
To figure out more about type constructors, they're basically the
same as value constructors, just for types. So, in haskell (not
talking about the extensions), each type has a kind. For
instance, all the types that don't take a type argument have the
kind *. On the other hand, all the types that take one
parametric type have the kind * -> *. Similarly,
types like Either have the kind
* -> * -> *. You get the gist. This is basically
a second order abstraction over types.
This is in particular, distinct from Rust where there are two base kinds, the type and the lifetime. They follow different semantics.
So basically a type constructor is a functor if you can take a function and use that to map the inner type in the type constructor's concretization to another type wrapped in the same type constructor.
I use the term wrapped very loosely over here, but get the point. It's basically the application of the type constructor on that type.
In terms of signature, it's defined as:
class Functor f where
fmap :: (a -> b) -> f a -> f b
The types make it obvious. You take the function and map it on the included type to yield a new wrapped type. It can also be seen this way: given a function, a functor wraps the input and the output of the function in its type constructor.
The Rules of Functors
A functor over an ID function should yield the same value. This seemingly comes from category theory, where non-bottom types form the category Hask, where functions are endofunctors over the category. I don't know much about what those words mean 😢.
Regardless, if this doesn't hold, then the type constructor is not a functor.
TL;DR:
fmap id = idholdsThe second rule is that two functions composed and then fed to the functor should lead to the two functions fed to the functor and then composed.
Applicatives
Applicatives are somewhat like functors on steroids. In haskell,
we don't differentiate between functions and regular types. In
essence, a function is just a value. Applicatives allow us to wrap
those up in the type-constructor as well and then use that to map
the same way fmap did.
class (Functor f) => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
pure, well, lifts the function/value from the base
type to the derived type, and the fancy operator does the exact same
thing as an fmap, except with functions that are
already wrapped by the type-constructor.
One very interesting example of the applicative typeclass is the
((->) r) type, which means, functions. In a way the
partial application of them.
The way to understand see
visualize interpret it would be to take the
-> as a typeclass taking two types and returning a
function that takes the former and returns the latter.
instance Applicative ((->) r) where
pure x = (\_ -> x)
f <*> g = \x -> f x (g x)
Over here, pure makes intuitive sense. The way to
lift a "value" to a function is, well, a thunk (ideally a thunk that
takes (), but beggars can't be choosers. We need the
type to be a generic cuz of type constraint, or the lack thereof, on
the type of pure).
The funky operator is, well, intriguing to say the
least. The types check out: the first is an f that's of
the type r -> a -> b, and the second argument is
of the type r -> a and the output is of the type
r -> b. x in the lambda is of the type
r. But what does it even mean??
Treat r as a shared state type between the
functions.
isAlphaNum :: Char -> Bool
isAlphaNum = pure (||) <*> isAlpha <*> isDigit
The same can also be interpreted as a functor (of course, because of the type constraints). In that case
fmapjust behaves as the composition operator. It should be straightforward to see why.
Monads
A Monad is basically representing a value in terms of a context. You might ask how this is different than Applicative that we discussed before. Honest answer, it's not. Except, it allows you to sequence things in a way where the effect of the next computation depends on the previous one.
Think about it, you can't do that with Applicatives. That's because the computations never have the unwrapped value of the previous one. They're totally independent in terms of computations. The pipeline is already built and statically determined. You can add a form of choice by having choices in a wrapper pure function that finally calls one or the other, but yeah, Monad adds that to the structure of the typeclass itself.
More importantly, the point I want to drive home is the fact that a monadic computation has access to the results of the previous computations in the pipeline. Applicative is really just carrying the argument wrapped in a typeclass to a prebaked function, which has the entire pipeline shoved in it.
class Applicative m => Monad (m :: * -> *) where
(>>=) :: m a -> (a -> m b) -> m b
return :: a -> m a
return behaves similar to pure, lifting
a value to a context. The other operator is also called
bind, as it binds the result of the previous
computation to a new one.
Monads give you a choice
Based on the outcome of the previous computation, you can change
the thing that you want to do in the new computation. This is a very
powerful abstraction. The same is clearly not possible in
Applicatives because the entire pipeline is decided statically. In
fact, they are arguments to a function, not even the function
itself. You can compose various functions but be prepared
for the branching thrown at your face when you have to make
decisions in each function. Monad's bind lets you deal
with it in a more structured fashion.
Choice at failure??
Notice that Monads give you no choice over what happens when a
computation fails. That's where the Alternative
typeclass comes in, which provides exactly that. It allows you to
choose between two different computations in case the first
one fails. It also expects you to have an empty
computation. This is because it leads to having a notion of an
impossible choice, and having the Alternative behave more like a
monoid. (A monoid is nothing but a typeclass for roughly the same
thing, except more of collection in the sense).
Monadic Laws
As with everything else, there are laws surrounding monads.
return a >>= k = k a: Lift a value into a contextual form, and bind it, and the result is the same as calling the function on it directly.m >>= return = m: Take a value in a context, bind it to return, and it'll give the value back. This is basically equivalent to unwrapping it from the context and wrapping it again.m >>= (\x -> k x >>= h) = (m >>= k) >>= h: Associativity of the effectful computations. Basically, as long as the sequence is maintained, the associativity doesn't matter.