I'm writing this short article based on two observations:
- The internet is filled with many "what is a monad" articles but comparatively few "what is a comonad" articles.
- These "what is a monad" articles are often written for frontend developers.
I'd like to set the record straight in this article by claiming, in the most hand-wavey and unscientific of ways, that frontend = comonad, backend = monad. So, if you are frontend engineer, learning about comonads is IMO the way to go.
This observation is not at all new. Phil Freeman's article The Future is Comonadic makes the point that working with UIs is tantamount to working with a comonad. So I'm writing this article mostly to amalgamate different strands of thought, clear up confusion, and put a name on certain industry trends.
Monad vs comonad
A monad m
providing a context for type a
is equipped with two operations:
pure :: a -> m a
join :: m (m a) -> m a
A comonad w
providing a context for type a
is equipped with two operations:
extract :: w a -> a
duplicate :: w a -> w (w a)
The only difference between the two is what's on the left and right of the arrow.
An example of a monad and a comonad
Let's take a quick gander at a monad and a comonad in the wild.
Our monad - Maybe
Maybe
is a monad that provides a context of existing or not existing to a value of type a
.
data Maybe a = Just a | Nothing
pure
for Maybe
is defined like this:
pure :: a -> Maybe a
pure = Just
We could have defined it like this:
pure :: a -> Maybe a
pure _ = Nothing
But that wouldn't be very nice, as it would destroy all the input values. While perfectly legal, the community has rallied around the first pure
with Just
because it is closer to our intuition about how pure
for Maybe
should behave.
join
is defined like this:
join :: Maybe (Maybe a) -> Maybe a
join (Just x) = x
join Nothing = Nothing
As maybes pile up, join gives us a way to squish them back down to a single level of Maybe
-ness.
Our comonad - Stream
Stream
is a comonad that provides an infinite list of values.
data Stream a = Stream a (Stream a)
In that context, extract
gets the head of the stream
extract :: Stream a -> a
extract (Stream head rest) = head
duplicate
gets the tail of the stream.
duplicate :: Stream a -> Stream (Stream a)
duplicate (Stream head rest) = Stream rest
You can then call extract
on the result of duplicate to get a Stream
and get its head with extract
.
Backend vs frontend
Backend
In backend development, there are two main tasks:
- Reigning in the chaos (API calls, flaky hardware, working with the filesystem, GPUs, blech...)
- Inserting our business logic, the only thing we "know", into this madness to produce value for someone somewhere.
These correspond to:
-
join
: Reigning in the chaos. -
pure
: Bringing our business logic to the party.
Frontend
In frontend development, there are two main tasks:
- Managing and anticipating possible future scenarios so that when a user does thing x or y it "just works".
- Rendering a UI for a user.
These correspond to:
-
duplicate
: Projecting into the future (the tail of our stream). -
extract
: Rendering a UI.
Community trends
Massive community trends can be boiled down to the comonad vs monad divide.
Backend: Kubernetes is a monad
Kubernetes is a monad. You call pure
on bits of your stack to get it into a pod, and you call join
on pods so that you never have kubernetes-in-kubernetes: there's always just one level.
Frontend: Nextjs is a comonad
Nextjs is a comonad. You call extract
to take JavaScript and make it a static webpage via its SSR, and React hooks are essentially duplicate
: they project a value into the future by returning a callback.
Ask for a comonad tutorial
So, if you're a frontend dev and rolling your eyes at "not another monad tutorial", ask for a comonad tutorial. Chances are it will be much more relevant to your day-to-day work!
Top comments (1)
I'm asking for a comonad tutorial