Best way to combine multiple state/reader monads?
Asked Answered
V

1

7

I am writing a project that involves composing several stacks of StateT and ReaderT monads:

newtype FooT m a = FooT { unFooT :: (StateT State1 (ReaderT Reader1 m)) a }

newtype BarT m a = BarT { unBarT :: (StateT State2 (ReaderT Reader2 m)) a }

Then, I basically just run everything in FooT (BarT m) and lift into the appropriate monad as necessary. I'm using lens to interact with the various state/reader types:

foo :: Monad m => FooT m ()
foo = do
  field1 .= ... -- where field1 is a lens into State1
  ...

However, this approach gets ugly as I add more StateT + ReaderT transformers (and seems like it might incur some performance costs).

My only idea so far is to combine the states like:

newtype BazT m a = BazT { unBazT :: StateT (State1, State2) (ReaderT (Reader1, Reader2) m)) a }

and then I can just project into the state types with more lenses.

foo :: Monad m => BazT m ()
foo = do
  (_1 . field1) .= ... -- where field1 is a lens into State1
  ...

Is there a canonical way to combine multiple states like this? If possible I'd like to avoid modifying all the lens code.

Volnay answered 27/7, 2022 at 16:50 Comment(3)
You've just encountered one of the most fundamental limitations of mtl-style transformer stacks. Are you familiar with any freer monad libraries like polysemy? Generally, when a project gets to the point of having complex stacks like this, the project outgrows mtl and moves onto a more sophisticated effect handling system.Personalty
Yeah, if I was starting from scratch I would definitely use a different architecture. In the short term I'm looking for a relatively low-impact solution. I'm also using LogicT to make the entire stack nondeterministic which is relatively trivial with the mtl 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
One thing I generally add to your approach is to create tiny classes associated with each field, with one method that returns the lens. Then you don't have to worry about the exact shape your state type eventually takes on.Erminois
F
0

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.

Fortunna answered 10/2, 2023 at 7:37 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.