In their paper “Extensible Effects”, Kiselyov, Sabry and Swords (KSS) introduce an effect system for Haskell that is an alternative to the classical monad transformers approach. There is a Haskell library of the same name implementing the ideas of the paper. Dan Doel noted in a recent article that this is one of many papers which investigate effects and their associated handlers, in large part aiming to move from what is seen to be the “static” approach of monad transformers, to the supposedly more “dynamic” approach of algebraic effects.
Dan’s article is a defence of the classical monad transformers approach, and he argues convincingly “that the staticness [of monad transformers] can actually be a useful property, that certain ways of harnessing it can make the confusing constructions less so, and … that algebraic effects perhaps aren’t so different from what we are already using”.
KSS give two examples where they claim their approach is superior to
monad transformers. The first is that it is awkward to combine
exceptions with non-determinism. However, Dan implements a simple
alternative to catchError and ensures that the exception handler
removes from the set of effects. In this way he efficiently disposes
with the supposed awkwardess. The second objection of KSS is that a
certain interleaving of a read effect with coroutines is impossible
to achieve with monad transformers. In this article I will
demonstrate that contrary to KSS’s assertion, this interleaving is
possible with monad transformers, and moreover straightforward.
catchError from the mtl
runs a monad transformer stack that contains an “exception raising”
layer and handles any exceptions with a handler that can potentially
rethrow them. The definition has the following signature
catchError :: MonadError e m => m a -> (e -> m a) -> m a
The monad m is the same in the argument and return value, thus any
exceptions that are rethrown are rethrown at exactly the same level of
the stack. As KSS note, this does result in a particular
inflexibility when mixing effects.
However, Dan realised that handlers should remove from the set of
effects. That is, the monad of the return value should be “smaller”
than the monad of the argument. He defines
localCatch
with the signature
localCatch :: Monad m => ErrorT e m a -> (e -> m a) -> m a
which handles exceptions at the outermost level and gives the user the chance to rethrow them at a lower level. Using this formulation yields the results that KSS are seeking with no further difficulties.
KSS provide the following type CoT which is a co-routine transformer
type CoT a m = ContT (Y m a) m
data Y m a = Done | Y a (() -> m (Y m a))
and define a function which mixes read effects with coroutines. The
idea is to use local to modify the local state of a read operation.
local has the signature
MonadReader r m => (r -> r) -> m a -> m a
and the function is
th3 :: MonadReader Int m => CoT Int m ()
th3 = ay >> ay >> local (+10) (ay >> ay)
where ay = ask >>= yield
They demonstrate that this has unexpected behaviour. They also consider the opposite order of effects
th4 :: Monad m => ReaderT Int (CoT Int m) ()
th4 = ay >> ay >> local (+10) (ay >> ay)
where ay = ask >>= lift . yield
but this does not work either. However, what I learned from Dan’s
article is that handlers should remove from the set of effects, so
instead of local we can try implementing localLocal (awkwardly
named to be consistent with Dan’s terminology!) which removes the read
effect.
localLocal :: MonadReader a m => (a -> r) -> ReaderT r m b -> m b
localLocal f m = ask >>= runReaderT m . f
Using localLocal highlights the problem. The ay outside the
localLocal needs to have a different type than the ay inside the
localLocal.
th3_explicit :: Monad m => CoT Int (ReaderT Int m) ()
th3_explicit = ay1 >> ay1 >> localLocal (+10) (ay2 >> ay2)
where ay1 :: Monad m => CoT Int (ReaderT Int m) ()
ay1 = lift ask >>= yield
ay2 :: Monad m => ReaderT Int (CoT Int m) ()
ay2 = ask >>= lift . yield
This implementation gives exactly the results that KSS are looking
for. Furthermore it’s not hard to remove the duplication by defining
a monad transformer class MonadCo which abstracts over continuation
monads and provides a yieldG operation (a transformer-polymorphic
version of yield). This leads to a neat and general implementation
th3_MTL :: (MonadCo Int m, MonadReader Int m) => m ()
th3_MTL = ay >> ay >> localLocal (+10) (ay >> ay)
where ay :: (MonadCo r m, MonadReader r m) => m ()
ay = ask >>= yieldG
With the right primitives, combining complicated effects using monad transformers can be straightforward. What I’ve presened here is not evidence that monad transformers are the best way to go, but it does show that more thought is needed before we declare them obsolete!
There are some further comparisons between extensible-effects and mtl.