In Short

Ether is a Haskell library that extends mtl and transformers with tagged monad transformers and classes. Here are some resources to learn about the concept:

Blog post Tagging Monad Transformer Layers by Dan Piponi

Paper PDF Monad Factory: Type-Indexed Monads by Mark Snyder, Perry Alexander

Features

Tagged effects—now you can have multiple MonadState s, MonadReader s, MonadWriter s and MonadExcept s in your monad transformer stack.

s, s, s and s 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.

The Code

Talk is cheap. Show me the code. — Linus Torvalds

See also Roman Cheplyaka's blog post The problem with mtl to see what exactly is the problem we're solving here.

Ether's interface is similar to mtl's interface, except most functions require a tag. So let's create some tags:

data Foo data Bar

Any type can serve as a tag. Later we'll see how it's used for implicit tagging.

But for now let's stick to our newly created tags Foo and Bar . We define a function that uses multiple MonadReader s (something impossible with mtl):

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

MonadReader

ask

runReader

runReaderT

n

30

30.0

Here we can see thatandare used just like the ones from mtl, only with a tag. As you might expect,here equals(or—the reader environment can be polymorphic).

Tagged versions of MonadState , MonadWriter and MonadExcept are provided as well. There's no tagged MonadCont because I couldn't think of a use case for multiple MonadCont s in a monad transformer stack.

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:

summator :: ( Num a , MonadWriter (Sum a) m ) => [a] -> m () summator xs = do for_ xs $ \x -> tell (Sum x)

MonadWriter

MonadWriter

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)

Now, say you want to add anotherto count how many numbers you've summed up. As we know, it's impossible with mtl alone, but you can combine the existing untaggedwith 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...

What if you have two functions, both requiring a MonadState , but it's different MonadState s? 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:

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)

tagAttach

Control.Monad.Trans.Ether.Dispatch

Here we use thefunction fromto 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 having one MonadExcept per exception:

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

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)

Now we can handle those exceptions one by one with. 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.