问题
So I have this sort of code all over my first serious haskell project:
f :: (MonadTrans t) => ExceptT () (t (StateT A B)) C
f = do mapExceptT lift $ do
lift $ do
...
lift $ do
...
r <- ...
...
return r
>>= \r -> ...
There definitely may be something wrong about how I try to achieve my goals (there might be simpler ways how to do it) but currently I am interested in learning how to handle a stack of monad transformers in some nicer way, if there is one. This is the only way I figured out how to get r
in the context of B
and lift it to a monad higher in the stack. Lifting whole blocks instead of initial statements is as far as I could get on my own.
What I also often end up with are chains of lift
which I found out can be avoided with liftIO
if the deep monad is IO
. I am not aware of a generic way for other monads though.
Is there a pattern that one can follow when he ends up dealing with such stacks, and having to extract one value at some level, a different value at a different level, combine these and affect any of the two levels or maybe yet another one?
Can the stack be manipulated somehow without neither lifting whole blocks (which causes let
and bound variables to be scoped and restricted to the inner block) nor having to lift . lift . ... lift
individual actions?
回答1:
This is a well-known problem with monad transformers in general. Researchers have devised various ways of handling it, none of which is clearly "best". Some of the known solutions include:
- The mtl approach, which automatically lifts type classes of monads over its built-in monad transformers (and only its built-in monad transformers). This allows you to just write
f :: (MonadState A m, MonadError () m) => m C
if those are the only features of the monad that your function is using. Due to its extreme non-portability and a few other reasons,mtl
is generally considered pseudo-deprecated. See this page and this question for the gory details. - If you have a particular monad stack you are using over and over again, you can wrap it in a
newtype
and write instances of the various monad type classes it supports manually. ForFunctor
,Applicative
,Monad
, and any other type classes implemented by the top-level transformer in your stack, you can useGeneralizedNewtypeDeriving
to have the compiler write the instances for you automatically; for other type classes, you will have to insert the appropriate number oflift
calls for each method. The advantage of this approach is that it's more general and simpler to understand while giving you the same flexibility at the call site asmtl
. The big problem with this approach is that it encourages using a single "mega-monad" for all operations rather than specifying only the needed operations, since adding any new monad transformer to the stack requires writing a whole new list of instances. - In most cases, you don't really want a monad that has "some arbitrary state of type
A
" and "some arbitrary exception-throwing capability". Rather, the different features offered by your monad stack have some semantic meaning in your mental model of your program. A variation of the previous approach is to create custom type classes for the effects beyond the basicFunctor
,Applicative
, andMonad
and write instances for the custom type classes on yournewtype
'd monad instead. This has a major advantage over the other approaches listed here: you can have a stack with multiple copies of the same monad transformer in it at different positions. This is the strategy I've used the most in my own programs so far. - A completely different approach is effect systems. Normally, an effect system has to be built in to a language's type system, but it's possible to encode an effect system in Haskell's type system. See the effect-monads package.
回答2:
The usual approach is to use the mtl
library rather than using transformers
directly. I'm not sure what the story behind your t
is, but the usual mtl
approach is to use very general type signatures at definition sites, like
foo :: (MonadError e m, MonadState s m) => m Int
Then fix the actual transformer stacks at the call sites. A common recommendation is to wrap up the stack in a newtype to avoid muddying things up where they're used.
If this isn't your style (and it's not for everyone), you can still use the mtl
methods to perform operations, while giving an explicit transformer stack. This should cut down on the manual lifting substantially. The advantage of this approach is that it gives you a better view of the interactions of effects at the definition sites; the disadvantage is that more code needs all the information.
来源:https://stackoverflow.com/questions/32551152/flatten-monad-stack