If you happen to work on both states in the same context a lot, you should combine these states, as they fit to one functionality.
A stack is typically encapsulating one functionality at a time. So, within one stack, you would generally need every monad transformer at most once.
To encapsulate a stack, you'll have to make sure that inner transformers are not exposed outside. That way, your stacks can be combined further.
Complete example on how to encapsulate a monad stack:
{-# LANGUAGE UndecidableInstances #-} -- Needed for all things around transformers.
import Control.Monad.Reader
import Control.Monad.State
import Control.Monad.Writer
import Control.Monad.Except
import Control.Applicative
data Reader1 = Reader1
data State1 = State1
newtype FooT m a = FooT { unFooT :: (StateT State1 (ReaderT Reader1 m)) a }
deriving
( Functor, Applicative, Monad, Alternative
, MonadWriter w, MonadError e, MonadIO
-- ..
)
-- Note that Reader and State are not derived automatically.
-- Instead, the instances are lifted past its inside manually.
instance MonadTrans FooT where
lift = FooT . lift . lift
instance MonadState s m => MonadState s (FooT m) where
get = lift $ get
put = lift . put
instance MonadReader r m => MonadReader r (FooT m) where
ask = lift $ ask
local f = mapFooT $ mapStateT $ mapReaderT $ local f
where
mapFooT g = FooT . g . unFooT
-- Your class that provides the functionality of the stack.
class Monad m => MonadFoo m where
fooThings :: m Reader1
-- ...
-- Access to the inside of your stack, to implement your class.
instance Monad m => MonadFoo (FooT m) where
fooThings = FooT $ ask
-- Boilerplate to include all the typical transformers,
-- so that MonadFoo can be accessed and derived through them.
instance MonadFoo m => MonadFoo (ReaderT r m) where
fooThings = lift $ fooThings
instance MonadFoo m => MonadFoo (StateT s m) where
fooThings = lift $ fooThings
-- ..... instances for all the other common transformers go here ..
-- Another stack, that can now derive MonadFoo.
data Reader2 = Reader2
data State2 = State2
newtype BarT m a = BarT { unBarT :: (StateT State2 (ReaderT Reader2 m)) a }
deriving
( Functor, Applicative, Monad, Alternative
, MonadWriter w, MonadError e, MonadIO
, MonadFoo
)
-- Bar class and related instances would follow here as before.
-- A new stack that can make use of Bar and Foo, preferably through their classes.
newtype BazT m a = BazT { unBazT :: BarT (FooT m) a }
-- Baz can have its own ReaderT and StateT transformers,
-- without interfering with these in FooT and BarT.
As you can see, it requires quite a lot of boilerplate code. You may omit some boilerplate, if its for internal code. If you write a library, your users will appreciate it though.
Various packages tackle the boilerplate issue.
mtl
-style transformer stacks. Are you familiar with any freer monad libraries likepolysemy
? Generally, when a project gets to the point of having complex stacks like this, the project outgrowsmtl
and moves onto a more sophisticated effect handling system. – PersonaltyLogicT
to make the entire stack nondeterministic which is relatively trivial with themtl
approach (and presumably possible with other architectures too, I just don't know what it would look like...). Though maybe migrating to polysemy or something is easier than I think it is, I'll look into it. – Volnay