One of the main deficiencies of transformers
is that the MonadTrans
class does not let you lift functions like catchError
. The mtl
library provides one solution to this problem, which is to type-class catchError
and throwError
using the MonadError
type class.
That's not to say that transformers
has no solution for lifting catchError
and 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.
For example, Control.Monad.Trans.State
provides the following liftCatch
function:
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 StateT
layer:
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
function:
catching
:: (Monad m, Error e)
=> ((ErrorT e m a -> (e -> ErrorT e m a) -> ErrorT e m a) -> r)
-> r
catching k = k catchError
The 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 MonadError
approach:
- You get improved type inference
- You get type errors earlier in development. With
MonadError
the compiler will not detect an error until you try to run your monad transformer stack. - You get better type errors.
MonadError
errors will arise at a distance where you callrunErrorT
even though the logical error is probably at the site of thecatchError
function. - 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 local
, listen
, and 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.
No comments:
Post a Comment