Do We Really Need All These Monad Transformers?

Since I first learned about them, I’ve been a fan of Monad Transformers. Just stick them all together and we can pretend we’re writing Java, who doesn’t want that? Mutable State, Logging, Configuration, Exceptions, they’re all there. Heck, stick some IO in that if you like. Apparently, there’s even a pre-built Reader/Writer/State Monad in there. However, lately I’ve been working on a fairly large Haskell project, and this project doesn’t use Monad transformers. And you know what? It’s working out for them just fine.

Lately, I’ve been wondering if all these transformers are really worth the effort? After you’ve chained together some massive runFoo (runBar (runBaz (runMonadRun ))) foobarbaz function, and got it all working right and not returning some ridiculous nested tuple, what have you gained?

ReaderT

First up is ReaderT. ReaderT let’s us carry around a read-only configuration. In an ReaderT Monad, we can do the following:

foo :: (MonadReader c m) => a -> m b foo a = do -- stuff config <- ask -- more stuff, presumably using config

…without having to have a config parameter on our function. This is nice because it improves readability. Right? Because this is so bad:

foo :: c -> a -> b foo c a = -- stuff, presumably using config

“But ReaderT gets you local!” you say:

foo :: (MonadReader c m) => a -> m b foo a = do -- stuff local modifyC bar where modifyC c = -- change the config bar = -- some monad

Nifty, I agree. Or we chould just do:

foo :: c -> a -> b foo c a = bar (modifyC c) where modifyC c = -- change the config bar c = -- some function of c

…which I believe is much clearer, because I didn’t have to go to Hackage to figure out what local does.

StateT

Conceptually kind of a combination of ReaderT and WriterT (I’m going to skip WriterT for the sake of brevity), StateT lets us use mutable state in a Monadic function:

foo :: (MonadState s m) => a -> m b foo a = do state <- get -- do stuff, presumably change the state put state' -- more stuff

So, what’s the non-monadic alternative? I imagine something like this:

foo :: s -> a -> (s, b) foo s a = (s', a') where s' = -- do something to change the state a' = -- do something using s'

I suppose that’d be workable, but now we have this tuple to deal with. We have a few options. We can do pattern matching:

bar :: a -> (s, b) bar a = (s', b') where s = -- some initial state (s', b) = foo s a b' = -- do something, using b but not s'

…or we can use something like uncurry:

-- uncurry :: (a -> b -> c) -> (a, b) -> c baz :: s -> a -> (s, b) baz a = foo (uncurry $ bar a)

Both of these are much harder to understand than the Monadic version in my opinion. For both, we have to shimmy our data to fit the interface, and these are just contrived examples.

ExceptT

Finally, I’d like to talk about ExceptT. This monad lets us simulate exceptions. Unlike Haskell’s normal exception system, where exceptions can only be caught in IO, these exceptions can be caught within the ExceptT monad:

crud :: (MonadError String m) => a -> m b crud a = throwError "Oh crud!" -- doesn't catch foo :: (MonadError String m) => a -> m c foo a = do -- stuff res <- curd a -- doesn't make it this far -- catches the exception bar :: (MonadError String m) => a -> m c bar a = do -- stuff res <- catchError handler (crud a) -- still rolling where handler e = -- exception handler

Seems reasonable right? Or, we could just use Either:

crud :: a -> Either String b crud a = Left "Oh crud!" -- must handle the error foo :: a -> c foo a = case (crud a) of Left e -> -- handle error Right c -> -- all good

Personally, I find these two to be a wash. Both are understandable. The Monadic version has the potential for the programmer to forget to handle the error, but the non-monadic version involved a noisy case statement.

Not As Clear As It Seemed Before

As you can see, these things are hit and miss. Having thought about it while typing this out, if I had some function that just needed to read a config and throw an error, I’d probably define it like this:

foo :: c -> a -> Either String b

…however, if I needed to throw some state in there:

foo :: (MonadReader c m, MonadState s m, MonadError e m) => a -> m b

Suddenly the ReaderT and StateT become more attractive; why not throw them on the stack? I suppose the point is that maybe using a huge transformer isn’t the best way, not that it certainly isn’t the best way. Just some food for thought.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: