When using multiple Reader/Writer/State transformers in the same monad stack, it becomes necessary to lift the operations in order to affect a specific transformer. Using heterogeneous lists (and all kinds of GHC extensions magic), this package provides transformers that remove that necessity: MultiReaderT/MultiWriterT/MultiStateT can contain a heterogeneous list of values.
The type inferred for the getter/setter determines which value is read/written.
simpleExample :: IO ()
simpleExample = runMultiStateTNil_ -- start with an empty state,
-- i.e. :: MultiStateT '[] IO
$ withMultiStateA 'H' -- "adding" a char to the state
$ withMultiStateA "ello, World!" -- and a string
$ do -- so:
-- the monad here is MultiStateT '[String, Char] IO
let combinedPrint = do -- no type signature necessary
c <- mGet -- type of mGet inferred to be m Char
cs <- mGet -- inferred to be m String
lift $ putStrLn (c:cs)
combinedPrint
mSet 'J' -- we modify the Char in the state.
-- again, the type is inferred,
-- without any manual lifting.
combinedPrint
The output is:
Hello, World!
Jello, World!
( you can find both this and a more complex example in an executable in the package. )
If you try to execute an action that requires a specific type in the state, but the current state does not contain that type, the error message is something like
No instance for (Control.Monad.MultiState.ContainsType Foo '[]) x
where Foo
is the missing type.
It is possible to run single-valued actions inside multi-valued
transformers using the inflate
functions. A function transforming
a multi-valued transformer with exactly one element into a
single-valued transformer would be trivial, but it is currently not provided.
(Will refer to StateT in this paragraph, but equally valid for Reader/Writer)
The mtl monad transformers make use of primarily three methods to "unwrap"
a transformed value:
runStateT
, evalStateT
, execStateT
. These three all have a type
matching the pattern s -> t m a -> m b
, they differ in what b
is.
We will use a different naming scheme, for three reasons:
-
"run", "eval" and "exec" are not in any way intuitive, and should be suffixed in any case.
-
For MultiStateT, it makes sense to transform an existing transformer, adding another state. The signature would be close to that of runStateT, only without the unwrapping part, i.e.
s -> t m a -> t' m b
, wheres
is the initial state, andt
ist'
with another state added. -
Sometimes you might want to add/run a single state, or a bunch of them. For example, when running an arbitrary StateT, you would need to provide a HList of initial states, and would receive a HList of final states.
Our naming scheme will instead be:
-
runStateT.*
unwraps a StateT. A suffix controls what exactly is returned by the function. There is a special version for when the list of states is Nil,runStateTNil
. -
withStateT.*
adds one or more states to a subcomputation. A suffix controls the exact return value.
withStates
/-------------------------------------------------------\
| withState withState .. withState v
StateT '[s, ..] m --------> StateT '[..] m --------> .. --------> StateT '[] m
| <-------- |
| (withoutState) |
| |
| |
| runStateT runStateTNil |
\--------------------> m .. <---------------------------/
Specific functions are (constraints omitted):
runMultiStateT = runMultiStateTAS
runMultiStateTA :: HList s -> MultiStateT s m a -> m a
runMultiStateTAS :: HList s -> MultiStateT s m a -> m (a, s)
runMultiStateTSA :: HList s -> MultiStateT s m a -> m (s, a)
runMultiStateTS :: HList s -> MultiStateT s m a -> m s
runMultiStateT_ :: HList s -> MultiStateT s m a -> m ()
runMultiStateTNil :: MultiStateT '[] m a -> m a
runMultiStateTNil_ :: MultiStateT '[] m a -> m ()
withMultiState = withMultiStateAS
withMultiStateA :: s -> MultiStateT (s ': ss) m a -> MultiStateT ss m a
withMultiStateAS :: s -> MultiStateT (s ': ss) m a -> MultiStateT ss m (a, s)
withMultiStateSA :: s -> MultiStateT (s ': ss) m a -> MultiStateT ss m (s, a)
withMultiStateS :: s -> MultiStateT (s ': ss) m a -> MultiStateT ss m s
withMultiState_ :: s -> MultiStateT (s ': ss) m a -> MultiStateT ss m ()
withMultiStates = withMultiStatesAS
withMultiStatesAS :: HList s1 -> MultiStateT (Append s1 s2) m a -> MultiStateT s2 m (a, HList s1)
withMultiStatesSA :: HList s1 -> MultiStateT (Append s1 s2) m a -> MultiStateT s2 m (HList s1, a)
withMultiStatesA :: HList s1 -> MultiStateT (Append s1 s2) m a -> MultiStateT s2 m a
withMultiStatesS :: HList s1 -> MultiStateT (Append s1 s2) m a -> MultiStateT s2 m (HList s1)
withMultiStates_ :: HList s1 -> MultiStateT (Append s1 s2) m a -> MultiStateT s2 m ()
withoutMultiState :: MultiStateT ss m a -> MultiStateT (s ': ss) m a
This package currently lacks a complete set of "lifting instances", i.e. instance definitions for classes such as mtl's MonadWriter "over" the newly introduced monad transformers, as in
instance (MonadWriter w m) => MonadWriter w (MultiStateT c m) where ..
These "lifting instances" would be necessary to achieve full compatibility with existing transformers. Ping me if you find anything specific missing.
See changelog.md