I've been experimenting with Codensity
lately which is supposed to relate DList
with []
among other things. Anyway, I've never found code that states this relation. After some experiments I ended up with this:
{-# LANGUAGE RankNTypes #-}
module Codensity where
newtype Codensity f a = Codensity
{ runCodensity :: forall b. (a -> f b) -> f b }
type DList a = Codensity [] [a]
nil :: DList a
nil = Codensity ($ [])
infixr 5 `cons`
cons :: a -> DList a -> DList a
cons x (Codensity xs) = Codensity ($ (xs (x:)))
append :: DList a -> DList a -> DList a
append (Codensity xs) ys = Codensity ($ (xs (++ toList ys)))
toList :: DList a -> [a]
toList xs = runCodensity xs id
fromList :: [a] -> DList a
fromList xs = Codensity (\k -> k xs)
However, the definition of DList
feels a bit icky in my example. Is there a different way to state this relation? Is this even the right way to do this?
One view could be that
DList
is a way for reordering monoid operations, just asCodensity
is a way for reordering monad operations.[]
is a free monoid ona
, so let's represent lists using a free writer monad, that isFree ((,) a)
:Now we can define the standard list operations:
This representation has the same drawbacks as list when it comes to
snoc
. We can verify thattakes a significant (quadratic) amount of time. However, just as every free monad, it can be improved using
Codensity
. We just replace the definition withand
toList
withNow the same expression is executed instantly, as
Codensity
reorders the operations, just like the original difference lists.1. The full story
The plan is as follows:
(++)
and(>>=)
, emonstrating the problem with associativity.DList
andCodensity
(++)
to(<>)
)2. The problem: quadratic runtime behaviour
2a. List
(++)
So let's first look at the problem with lists. The concat operation for lists is commonly defined as:
which means that
(++)
will always walk the first argument from start to end. To see when this is a problem consider the following two computations:Let's start with
rightAssoc
and walk through the evaluation.So we have to walk over
as
andbs
.Okay that was not too bad, let's continue to
leftAssoc
:Uh oh, did you see that we had to walk over
as
twice? Once as[1,2]
and then again insideas ++ bs = [1,2,3,4]
. With each further operand that is wrongly associated, the list on the left of(++)
which we have to traverse completely each time will grow longer in each step, leading to quadratic runtime behaviour.So as you see above left-associated
(++)
will destroy performance. Which leads us to:2b. Free monad
(>>=)
First, we use the naive
Free
type:Instead of
(++)
, we look at(>>=)
which is defined as and use(>>=)
in prefix form:If you compare this with the definition of
(++)
from2a
above, you can see that the definition of(>>=)
again looks at the first argument. That raises a first concern, will this perform as bad as in the(++)
case when associated wrongly? Well, let's see, I useIdentity
here for simplicity but the choice of the functor is not the important fact here:We again start with the
rightAssoc
variant first:Puh, okay I added
~~~~
under the expression that is reduced in the next step for clarity. If you squint hard enough, you may see some familiarity from2a
's' case forrightAssoc
: we walk the two first arguments (nowx
andf
instead ofas
andbs
) arguments once. Without wasting further time, here isleftAssoc
:If you look close, after the
uh oh
we have to tear down the intermediate structure again, just like in the(++)
case (also marked withuh oh
).2c. Result so far
In both cases,
leftAssoc
leads to quadratic runtime behaviour, because we rebuild the first argument several times and tear it down right again for the next operation. This means that at each step in the evaluation we have to build and tear down a growing intermediate structure --- bad.3. The relation between
DList
andCodensity
This is where we will discover the relation between
DList
andCodensity
. Each one solves the problem of wrongly associated computations seen above by using CPS to effectively reassociate to the right.3a. DList
First we introduce the definition of
DList
andappend
:and now our old friends:
Evaluation is roughly as follows:
Hah! The result is exactly the same as
rightAssoc
from2a
. Allright, with tension building up, we move on toleftAssoc
:Heureka! The result is associated correctly (to the right), no quadratic slowdown.
3b. Codensity
Okay if you've come to this point you must be seriously interested, that's good, because so am I :). We start with the definition and
Monad
instance of Codensity (with abbreviated names):I guess you know what comes next:
Before we go through the evaluation once again, you might be interested in the comparison of
append
fromDList
and(>>=)
fromCodensity
(unDL
~
run
), go ahead and do that if you want, I'll wait for you.Okay we start with
rightAssoc
:And we can now see that the
(>>=)
s are associated to the right, this is not yet particularly astonishing, given that this was also the case at the start. So, full of anticipation, we turn our attention to our last and final evaluation trace,leftAssoc
:Finally there we have it, all binds associated to the right, just how we like them!
4. Aftermath
If you made it until here, congratulations. Let's summarize what we did:
(++)
in2a
and(>>=)
in2b
DList
in3a
andCodensity
in3b
.5. Bonus
Actuall, we can generalize
DList
from(++)
and use(<>)
instead to getDMonoid
, reordering(<>)
.Then the comparison goes as follows:
DMonoid
is a (my invention) "monoid transformer", reassociating(<>)
to the rightCodensity
is a monad transformer, reassociating(>>=)
to the right