Contravariance and the Hidden Purescript Pacman Operator

The Pacman operator remains the least recognized operator in all of functional programming. Hidden right under our noses, he munches away at a tasty pellet, and only when we turn away does he go after a few ghosts.

I was recently reminded of his elusiveness while watching the following talk on Profunctors. In the talk, it takes all of 1 minute for everybody to grok covariant functors, and another 12 minutes to thoroughly confuse everybody about the contravariant ones.

Waka! Waka! Waka!

And again I was reminded how much simpler concepts like contravariance would be if the Pacman operator wasn’t quite so well hidden. So, let’s do our best to unmask this 80’s superstar and gain some insight in the process.

The Pacman Operator You Never Knew was There

So what is the Pacman operator and how does he hide in plain view? Well, we all understand the arrow operator, as we see it every day.

(a -> b)

It is self evident from the above function signature that we can use this function to produce b, when given a. And so map becomes equally easy for somebody to understand.

map :: (a -> b) -> f a -> f b

Given a function(a -> b) that produces b’s from a’s, we innately understand that we can unwrap an f a, apply the function to produce b, and wrap the b back up into an f b.

We are hardwired to be productive when programming, we are producers, creators, and the arrow operator places nicely into this bias, and the world and the programmers in it are content and happy.

However, as I’m sure you’ve figured out by now, there is an alternate way to read the above signature.

(a -> b)

Instead of reading the signature from left to right, we’ll read it from right to left. Heresy, you say! Tis true, but if you squint, and forget what you know, you’ll be able to see him right there in plain sight.

For while the arrow operator let’s us view functions through the lens of production, the Pacman operator let’s us view functions through the lens of consumption.

When read from right to left, the function(a -> b) reads as b’s consuming a’s, the arrow we see every day magically transforms into Pacman eating a pellet and what ever comes before him.

Waka! Waka! Waka! Um num num!

And with this simple change in direction and mindset, previously difficult concepts, becomes much easier to understand.

Pacman Makes Contravariance Easy

So now we’ve begun reading(a -> b) from right to left, that b’s really like munching down on those tasty morsels of a, it should come as no surprise when handed an f b, we can simply unwrap it, extend b with our (a -> b) function, that says b’s can consume a’s, and wrap the whole thing back up.

cmap :: (a -> b) -> f b -> f a

So that’s exactly what we’ll do here.

type FX = forall e. Eff (console :: CONSOLE | e) Unit
data Pacman b = Munch (b -> FX)
instance contraPacman :: Contravariant Pacman where
cmap a2b (Munch b) = Munch (b <<< a2b)

All we’ve had to here(b <<< a2b)was prepend our (a -> b) function in order to consume a’s. Easy, just as natural and easy to understand as Functor!

Our left to right bias, and always viewing -> as the production arrow, was the only thing holding us back. And it’s not just contravariance where Pacman can help us out. Once we know he’s there, we start to find uses for him all over.

Pacman Helps Us Understand Tricky Type Signatures

If you’re having trouble understanding a type signature, then try reading the signature from right to left, using the Pacman operator, and focusing on consumption instead of production.

For instance, I find uncurry a bit easier to understand in terms of consumption.

uncurry :: (a -> b -> c) -> (a, b) -> c
uncurry f (a, b) = f a b

If we read the signature from right to left, we can ask the question, how does my tuple consume my function?

Well that’s easy, first I apply the function to my first item, then apply it to my second item. All gone! Waka Waka Waka!

Pacman Helps with the Intuition of What Things Do

Sometimes in functional programming, it can be helpful to have a high level notion of what things do. For instance, let’s take a look at the return function in Monad.

class Monad m where
return :: a -> m a

Pacman tells us that ma’s like to munch on a’s, so a monad is something that consumes values and leaves structure in it’s place!

Similarily for comonad,

class Comonad w where
extract :: w a -> a

Pacman tells us that our value a likes to munch on our wa, and so a comonad can be thought of something that munches on structure and leaves values in it’s place!

Waka! Waka! Waka!

Hopefully, the next time you see a Pacman arcade machine when out at your local dive joint, you’ll give him a nod and thank him for helping you out grokking monad and comonad.

Just remember to toss him a power pellet on the way out!