Source on GitHub

Ether

Monad Transformers and Classes

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:

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

Features

  • Tagged effects—now you can have multiple MonadStates, MonadReaders, MonadWriters and 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.

The Code

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:

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 MonadReaders (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
Here we can see that MonadReader, ask, runReader and runReaderT are used just like the ones from mtl, only with a tag. As you might expect, n here equals 30 (or 30.0—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 MonadConts 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)
Now, say you want to add another MonadWriter to count how many numbers you've summed up. As we know, it's impossible with mtl alone, but you can combine the existing untagged MonadWriter with a tagged one:
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)
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 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:

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)
Here we use the tagAttach function from Control.Monad.Trans.Ether.Dispatch to 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)
Now we can handle those exceptions one by one with 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:
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)
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.