Reader and Writer via Cont
We cannot easily produce a monad based on Cont
that behaves exactly like
Reader
. This may be surprising at first, but it isn't if you think about it.
Our implementation of the StateC
monad isn't a proper state monad either, in
the sense that it can do everything that the State
monad can do, but it can do
more! The State
monad allows us to thread state through the computation, but
it doesn't support callCC
or any other shenanigans using continuations. Given
that StateC
is just Cont
with a particular choice of the result type r'
,
we have all the power of the Cont
monad at our disposal.
When we introduced the Reader
, State
, and Writer
monads, we talked about
that technically, the State
monad would be enough, because we can use State
to implement any computation that uses the Reader
monad, simply by using get
in place of ask
, and never using put
, and similarly we could implement a
version of tell
on top of the State
monad as
tell :: Monoid w => w -> State w ()
tell w = modify (<> w)
The reason why we wanted to have three distinct monads was that it better
communicated our intent to the reader of our code. If we use the Reader
monad,
then we know that there is just no way that the computation modifies the
read-only context. This makes it much easier to understand the code than if we
have to trace all the state updates some code in the State
monad performs.
By using Cont
to implement State
, we obtain a monad that supports all the
operations that State
supports, but it's a monad that can do more. Thus, we
have already given up on using a monad that can do exactly what we need, nothing
less but also nothing more. After all, we are in a fictitious world where we
aren't allowed to define new monads. IO
and Cont
are the only monads we get.
Our goal here is not to build specializations of Cont
that behave exactly like
other monads but to demonstrate that we can use Cont
to do all those things we
can do using other monads.1
So, given that the Reader
monad is nothing but a State
monad that doesn't
give us the ability to modify the state, we can define a Cont
-based version
exactly like our Cont
-based state monad StateC
. Only we don't provide an
equivalent of put
:
type ReaderC r s a = Cont (s -> r) a
ask :: ReaderC r s s
ask = Cont $ \k s -> k s s
runReader :: ReaderC r s r -> s -> r
runReader = runCont m id
Similarly, as we observed above, we can simulate the Writer
monad as a State
monad where the state is some monoid w
, and we can implement tell
by using
modify
to update the state. Thus, we can also obtain a version of Writer
on
top of Cont
:
type WriterC r w a = Cont (w -> r) a
tell :: Monoid w => w -> WriterC r w ()
tell w = do
w' <- get
put (w' <> w)
runWriter :: Monoid w => WriterC (r, w) w a -> (r, w)
runWriter f = runCont f (,) mempty
Here, get
and put
are implemented as for the StateC
monad. We can obtain a
more direct implementation by reviewing what tell
is supposed to do: It takes
the current state w'
and replaces it with w' <> w
. The return value is ()
.
Our implementation of such a function using the StateC
monad would have been:
tell :: Monoid w => StateC r w a
tell w = Cont $ \k w' -> k () (w' <> w)
Since StateC r w a
and WriterC r w a
are defined identically, once we
replace StateC
with WriterC
, we obtain a more efficient implementation of
tell
for the WriterC
monad:
tell :: Monoid w => WriterC r w a
tell w = Cont $ \k w' -> k () (w' <> w)
-
Of course, this is a silly discussion to have in a sense because, if we really only want to be able to do what we can do using certain monads, then we don't need monads at all. We can just use pure functions. Remember, all monads other than
IO
andST
are just convenient abstractions implemented via pure functions. The point is that once we implement the right primitives, our implementations of the various monads on top of theCont
monad are just as convenient to use as the monads we're simulating. ↩