One of the main deficiencies of
transformers is that the
MonadTrans class does not let you lift functions like
mtl library provides one solution to this problem, which is to type-class
throwError using the
MonadError type class.
That's not to say that
transformers has no solution for lifting
throwError; it's just really verbose. Each module provides a
liftCatch function that you use to lift a
catchError function from the base monad to the transformed monad.
Control.Monad.Trans.State provides the following
Control.Monad.Trans.State.liftCatch :: (m (a, s) -> (e -> m (a, s)) -> m (a, s)) -> StateT s m a -> (e -> StateT s m a) -> StateT s m a
If your monad transformer stack was:
myStack :: (Monad m, Error e) => StateT s (ErrorT e m)
... then you would apply
liftCatch directly to
catchError to get a catch function that worked at the
Control.Monad.Trans.State.liftCatch catchError :: (Monad m, Error e) => StateT s (ErrorT e m) a -> (e -> StateT s (ErrorT e m) a) -> StateT s (ErrorT e m) a
But what if you had a
WriterT layer in between like this:
myStack :: (Monad m) => StateT s (WriterT w (ErrorT e m))
You'd have to use
liftCatch from the
Control.Monad.Trans.Writer module to further lift the
catchError to work with this stack:
import Control.Monad.Trans.State as S import Control.Monad.Trans.Writer as W S.liftCatch (W.liftCatch catchError) :: (Monad m, Error e) => StateT s (WriterT w (ErrorT e m)) a -> (e -> StateT s (WriterT w (ErrorT e m)) a) -> StateT s (WriterT w (ErrorT e m)) a
The advantage of this solution is that type inference works extraordinarily well and it is Haskell98. The disadvantage of this solution is that it is very verbose.
So I will propose a less verbose solution that has a syntax resembling the
lens library, although there will be no true lenses involved (I think). Define the following
catching :: (Monad m, Error e) => ((ErrorT e m a -> (e -> ErrorT e m a) -> ErrorT e m a) -> r) -> r catching k = k catchError
k in the above function will be a series of composed
liftCatch functions that we will apply to
catchError. However, in the spirit of the
lens library we will rename these
liftCatch functions to be less verbose and more hip and sexy:
import qualified Control.Monad.Trans.Maybe as M import qualified Control.Monad.Trans.Reader as R import qualified Control.Monad.Trans.State as S import qualified Control.Monad.Trans.Writer as W import qualified Pipes.Lift as P state = S.liftCatch writer = W.liftCatch reader = R.liftCatch maybe = M.liftCatch pipe = P.liftCatchError
Now let's say that we have a monad transformer stack of type:
myStack :: (Monad m, Error e) => StateT s (MaybeT (ErrorT e m)) r
If I want to catch something at the outer
StateT level, I would just write:
myStack = catching (state.maybe) action $ \e -> do -- The handler if `action` fails ...
If I add more layers to my monad transformer stack, all I have to do is point deeper to the stack by composing more references:
myStack :: (Monad m, Error e, Monoid w) => Pipe a b (StateT s (WriterT w (MaybeT m))) r myStack = catching (pipe.state.writer.maybe) action $ \e -> ...
Also, these references are first-class Haskell values, so you can bundle them and manipulate them with your favorite functional tools:
myStack = do let total = pipe.state.writer.maybe catching total action1 $ \e -> ... catching total action2 $ \e -> ...
This approach has a few advantages over the traditional
- You get improved type inference
- You get type errors earlier in development. With
MonadErrorthe compiler will not detect an error until you try to run your monad transformer stack.
- You get better type errors.
MonadErrorerrors will arise at a distance where you call
runErrorTeven though the logical error is probably at the site of the
- Functional references are first class and type classes are not
However, we don't want to lift just
catchError. There are many other functions that
transformers can lift such as
callCC. An interesting question would be whether this is some elegant abstraction that packages all these lifting operations into a simple type in the same way that lenses package getters, setters, traversals, maps, and zooms into a single abstraction. If there were, then we could reuse the same references for catching, listening, and other operations that are otherwise difficult to lift:
listening (state.error) m passing (maybe.writer) m catching (writer.maybe) m $ \e -> ...
I have no idea if such an elegant abstraction exists, though, which is why I'm writing this post to solicit ideas.