[Haskell] monad transformers

ajb at spamcop.net ajb at spamcop.net
Sun Jan 30 19:33:09 EST 2005


G'day all.

Quoting John Meacham <john at repetae.net>:

> However, now we come to the crux, I want
> the various interesting monad operations (MonadReader, MonadWriter,
> etc.. ) to be able to pass through StatT [...]

Why do you want to do that?  Think carefully before you answer.

I've never liked the O(n^2) instances.  First off, it's unmaintable.
Secondly, it's extremely fragile (all of your carefully-written
code which uses "read" is going to fail when you need to stack on another
Reader for some small part of the code).  Thirdly, and most importantly,
the "raw" monad operations are almost certainly meaningless and sometimes
are misleading.

Your "mticks" operation is a perfect example of a good operation.  That
means something that a mere "tell" does not.  Indeed, when you have
"mticks", you don't need "tell" at all.  So, quite correctly, you don't
expose it.  Why should things be any different for the other raw monad
operations?

There are some exceptions, of course.  I think that MonadIO, for example,
is a very reasonable class to expose because it's unambiguous, meaningful
and robust.

OK, now here's the way that I do it.

First, some state values:

    data RState = {- detail omitted -}
    data WState = {- detail omitted -}
    newtype CounterState = CounterState Int   -- Example

    instance Monoid WState where {- detail omitted -}

Now we can create our monad:

    type MyMReader = ReaderT RState IO
    type MyMCounter = StateT RWState MyMReader
    type MyMWriter = WriterT WState MyMCounter

    newtype MyM a = MyM (MyMWriter a) deriving (Monad, MonadIO)

And some internal lift operations (not exported to the MyM clients!):

    liftMyMtoWriter :: MyMWriter a -> MyM a
    liftMyMtoWriter = MyM

    liftMyMtoCounter :: MyMCounter a -> MyM a
    liftMyMtoCounter = liftMyMtoWriter . lift

    liftMyMtoReader :: MyMReader a -> MyM a
    liftMyMtoReader = liftMyMtoCounter . lift

Then you can define your meaningful operations:

    incrementCounter :: MyM Int
    incrementCounter
        = liftMyMtoCounter (do
            RWState x <- get
            put (RWState (x+1))
            return x
        )

Using the internal lift operations and exposing only meaningful operations
gives both you and the client insulation against the transformer stack
changing, which it will.  As an added bonus, it lets you define your own
"lift" instance if that makes sense:

    type MyMReader m = ReaderT RState m
    type MyMCounter m = StateT RWState (MyMReader m)
    type MyMWriter m = WriterT WState (MyMCounter m)

    newtype MyM m a = MyM (MyMWriter m a) deriving (Monad, MonadIO)

    liftMyMtoWriter :: (Monad m) => MyMWriter m a -> MyM m a
    liftMyMtoWriter = MyM

    liftMyMtoCounter :: (Monad m) => MyMCounter m a -> MyM m a
    liftMyMtoCounter = liftMyMtoWriter . lift

    liftMyMtoReader :: (Monad m) => MyMReader m a -> MyM m a
    liftMyMtoReader = liftMyMtoCounter . lift

    liftMyMtoBase :: (Monad m) => m a -> MyM m a
    liftMyMtoBase = liftMyMtoReader . lift

    instance MonadTrans MyM where
        lift = liftMyMtoBase

> *** Monad transformers rock.

Yes they do, but never forget that they're building blocks for monads
which have meaning on their own, independent of implementation.

Cheers,
Andrew Bromage


More information about the Haskell mailing list