Cayley Representation of... Monads?

In Haskell we’re using monoids intensively - monoid is an algebraic structure that has associative binary operation with neutral element.


class Monoid a where
  mempty :: a 
  mappend :: a -> a -> a

For instance, we have lists that form monoid under list concatenation and empty list as neutral element.

Short Tale About Lists

You’re a programmer in some software house that creates embeddable software for intelligent wash machines — in Haskell. Your task is to create reverse function that reverses the list, for example:


reverse [] = []
reverse [3, 2, 1] = [1, 2, 3]
reverse [2, 2, 3, 3] = [3, 3, 2, 2]

If you know Haskell in essential extent, you just write function like this:


reverse :: [a] -> [a]
reverse [] = []
reverse (a : as) = reverse as ++ [a]

You committed the code and you’re using it every day in your software, but there is a problem. Your function is pretty slow on long lists.

Why? Let’s look at list concatenation.


(++) :: [a] -> [a] -> [a]
[] ++ bs = bs
(a : as) ++ bs = a : (as ++ bs)

Calling reverse on [1, 2, 3, 4, 5] will create following thunk:


reverse [1, 2, 3, 4, 5] 
  = reverse [2, 3, 4, 5] ++ [1]
  = (reverse [3, 4, 5] ++ [2]) ++ [3]
  = ((reverse [4, 5] ++ [3]) ++ [2]) ++ [1]
  = ...
  = (((([] ++ [5]) ++ [4]) ++ [3]) ++ [2]) ++ [1]

Each call of (++) at n-th number takes 5 - n + 1 steps to concatenate [n] list. So reverse is O(n^2). Not good.

What if we can encode each action as a function that operates on list? We call these function as differential list, because we encode only changes in list rather than all content in one place.


type DList a = [a] -> [a]

empty :: DList a
empty = id

singleton :: a -> DList a
singleton a = (a :)

concat :: DList a -> DList a -> DList a
concat f g = f . g

toList :: DList a -> a
toList f = f []

Now we can implement reverse using DList.


reverse :: [a] -> [a]
reverse = toList . go
  where
    go :: [a] -> DList a
    go [] = empty
    go (a : as) = go as `concat` singleton a

We replaced (++) with basically (.) so concatenation time (with left associativity) is constant in comparison to lists’ lengths. This brings significant runtime improvement that we wanted to achieve.

DList is Cayley representation of list monoid. What does that mean? Cayley’s theorem says that every monoid M is isomorphic to the submonoid of automorphisms M -> M.

For sure DList fits in the Cayley’s theorem.

The moral of this story is that sometimes operation of “original” monoid can be not effective enough, so we can switch to the Cayley representation that is more preformant. Also you can notice that during building complex object using Cayley’s representation we can’t “peek” our object during building process. So this is kind of trade-off using this technique.

Cayley for Monads

Now we can see that Cayley’s theorem not only applies for classic monoids. We can consider, of course, monads, because monad is a monoid in category of endofunctors under functor composition.


class Functor m => Monad m where
  return :: a -> m a
  (>>=)  :: m a -> (a -> m b) -> m b

But this is not easy task since this category notably differs from Hask category. This category is called [Hask, Hask]. Our goal will be deriving the exponential object (every exponential in Hask is a function). Then having monad M we’ll use M^M as Cayley’s representation of monad M.

We know that every function can be curried and uncurried, this forms one of famous adjunctions, that can be described as Hom(a * b, c) ~= Hom(a, c^b).

Also we know that every Hask functor can be represented using Yoneda lemma, that is, functor instance FA is isomorphic to the natural transformation from functor Hask(A, -) to functor F.

Now we can connect these two facts to derive functor M^N A.


M^N A ~= Nat(Hask(A, -), M^N)       # by Yoneda lemma
      ~= Nat(Hask(A, -) `o` N, M)   # by "curry" adjunction, `o` means composition
   

So using this we can define Exp data type:


newtype Exp m n a = Exp { appExp :: forall r. (a -> n r) -> m r }

(Natural transformations are type f ~> g = forall r. f r -> g r in Haskell).

A functor M^M is a codensity monad, widely used with free monads. Since free monads are just like lists in runtime, but more restrictive by design, using Cayley’s representation on it can improve time spent on creating a list of actions of this free monad. This concludes the article.