Monad Transformers and Classes
- Blog post Tagging Monad Transformer Layers by Dan Piponi
- Paper PDF Monad Factory: Type-Indexed Monads by Mark Snyder, Perry Alexander
Tagged effects—now you can have multiple
MonadExcepts in your monad transformer stack.
- Mix tagged and untagged effects with no effort—regular transformers and classes can remain in your monad transformer stack.
- Turn untagged effects into tagged ones by wrapping them—no need to rewrite functions that use mtl.
- Different tagging styles (explicit and implicit)—no need to bother with tags if your types are unique.
- Simulate Java's checked exceptions, only better—list all exceptions your function can throw in its signature and handle them one by one.
Talk is cheap. Show me the code.
Ether's interface is similar to mtl's interface, except most functions require a tag. So let's create some tags:
Any type can serve as a tag. Later we'll see how it's used for implicit tagging.
data Foo data Bar
But for now let's stick to our newly created tags
Bar. We define a function that uses multiple
MonadReaders (something impossible with mtl):
Here we can see that
add :: ( Num a , MonadReader Foo a m , MonadReader Bar a m ) => m a add = liftA2 (+) (ask @Foo) (ask @Bar) n :: Num a => a n = runReader @Foo (runReaderT @Bar add 10) 20
runReaderTare used just like the ones from mtl, only with a tag. As you might expect,
30.0—the reader environment can be polymorphic).
Tagged versions of
MonadExcept are provided as well. There's no
MonadCont because I couldn't think of a use
case for multiple
MonadConts in a monad transformer
Ether's classes are fully compatible with ones from mtl, meaning that you can use Ether with your existing code without unnecessary changes. Consider the following code snippet:
Now, say you want to add another
summator :: ( Num a , MonadWriter (Sum a) m ) => [a] -> m () summator xs = do for_ xs $ \x -> tell (Sum x)
MonadWriterto count how many numbers you've summed up. As we know, it's impossible with mtl alone, but you can combine the existing untagged
MonadWriterwith a tagged one:
Easy, right? Untagged classes can be considered a special case of tagged ones, and any mix of them should Just Work™. But wait, there's more...
import qualified Control.Monad.Ether.Writer as Ether summator :: ( Num a , MonadWriter (Sum a) m , Ether.MonadWriter Foo (Sum a) m ) => [a] -> m () summator xs = do for_ xs $ \x -> do tell (Sum x) Ether.tell @Foo (Sum 1)
What if you have two functions, both requiring a
but it's different
MonadStates? There's no way you could
use those functions in one monad transformer stack with mtl, thus
mtl is antimodular!
However, Ether comes to the rescue with its wrapping mechanism:
Here we use the
f :: MonadState Int m => m String f = omitted g :: MonadState Bool m => m String g = omitted useboth :: ( Ether.MonadState Foo Int m , Ether.MonadState Bar Bool m ) => m String useboth = do a <- tagAttach @Foo f b <- tagAttach @Bar g return (a ++ b)
Control.Monad.Trans.Ether.Dispatchto turn untagged transformers into tagged ones.
On this wave of modularity features I'd like to present you another
one: simulating checked exceptions from Java. It's as simple as
MonadExcept per exception:
Now we can handle those exceptions one by one with
data DivideByZero = DivideByZero data NegativeLog = NegativeLog logDiv ( Floating a , Ord a , MonadExcept Foo DivideByZero m , MonadExcept Bar NegativeLog m ) => a -> a -> m a logDiv x y = do when (y == 0) (throw @Foo DivideByZero) let d = x/y when (d < 0) (throw @Bar NegativeLog) return (log d)
runExceptT. However, this tagging business starts to become unmanagable. Do we also need to create a tag per exception? The answer, fortunately, is no: since exceptions tend to be monomorphic, we can use them as tags for their classes. This feature is called implicit tagging, and we can rewrite the example above like so:
That's better. In fact, implicit tagging can be used with polymorphic tags too, but its behavior can be sometimes unobvious. You also may need to use type annotations when using implicit tagging, so for anything polymorphic prefer the explicit style.
import qualified Control.Monad.Ether.Implicit.Except as I logDiv ( Floating a , Ord a , I.MonadExcept DivideByZero m , I.MonadExcept NegativeLog m ) => a -> a -> m a logDiv x y = do when (y == 0) (I.throw DivideByZero) let d = x/y when (d < 0) (I.throw NegativeLog) return (log d)