Swiss-Army Knife Transformer Stack

Haskell

Characteristics

Scope: Structural AKA: MTL Style Monad Transformers. Safety: See "Pick your Poison" section

Explanation

A stack of monad transformers where you can keep adding transformers with different functionality, and all of them are immediately accessible by accessor functions, without any concern about lifting or ordering.

The MTL library is the reference implementation for this one. There are type classes with the same interface you expect from the transformers, but that are lifted by instantiating the transformers upper on the stack:

   statefullWriterFunction :: (MonadState Foo m, MonadWrite Bar m) => m ()
   statefullWriterFunction = do
       -- Now, anywhere we can get the state as:
       st <- get
       -- st :: Foo
       -- Or we can get write in the writer:
       let t = mempty :: Bar
       tell t

It is impossible to implement this pattern for some stacks, but when it is possible, it's a very clean way to deal with deep transformer stacks (altough one might want to question the choice of creating a deep transformer stack at all). This pattern is not a troublemaker at all, although its implementation is not perfect.

Finally, one can not repeat the same typeclass through the stack. The following code may look correct, but it does not work:

  aFunction :: (MonadState Foo m, MonadState Bar m) => m ()
  aFunction = -- something that uses Foo and Bar values

When resolving a call to a function of a class, GHC will ignore parameters, and simply pick one of the two class declarations and use it. If the type matches, it will compile, if not, it won't. It won't ever try the other declaration. When it compiles, it runs, but it doesn't always compile.

If you want to stack the same class with different parameters, you'll need to define a new class, with specialized functions, newtype the original implementation you want to stack, and NewtypeDerive the desired functionality (at a minimum Monad and MonadTrans). This is a some boilerplate, but just once at the class declaration, so it is manageable.

Implementation - Pick your Poison

As I said before, all the trouble is on the implementation. For GHC to be able to lift operations through a transformer, one must instantiate the class for every transformer used on the code. That is a lot of boilerplate, nearly all of it on the format:

   instance MyMonadClass MyMonadT where
       foo = lift foo
       bar = lift . bar

For each class, for each transformer... that is the way MTL is written, and the only safe way to make it.

But, if one can guarantee no orphan instances will be declared, most cases of this can be reduced into two overlapping instances:

  instance {-# OVERLAPPING #-} MyMonadClass MyMonadImplementationT where
    foo = -- actual implementation of foo
    bar = -- actual implementation of bar
  
  instance {-# OVERLAPPABLE #-} (MyMonadClass m, MonadTrans t,
    Monad (t m)) => MyMonadCladd t m where
    foo = lift foo
    bar = lift . bar

This makes big stacks of very specialized transformers viable. But keep in mind that mixing overlapping and orphan instances is really Bad, with capital "B".

GHC will help you avoiding orphan instances if you use -Wall. If those are not mixed, this pattern is pretty secure.