I have two or more independent states to track in one Haskell application.
I am declaring two new type classes using
type MonadTuple m = MonadState (Int, Int) m
type MonadBool m = MonadState Bool m
The monad transformer stack is declared as
type Stack = StateT (Int, Int) (StateT Bool IO) ()
I intend to use the stack as such
ret :: Stack
ret = apply
apply :: (MonadTuple m, MonadBool m) => m ()
apply = undefined
The complier is unhappy because it cannot match Bool
with (Int, Int)
when trying to check if Stack
conforms to MonadBool
.
I am aware of the solution given in the Combining multiple states in StateT. Are there any simpler solutions other than arrows or global state with lens?
Appendix: The full code block is
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE FlexibleContexts #-}
import Control.Monad.State.Class
import Control.Monad.State.Lazy
type MonadTuple m = MonadState (Int, Int) m
type MonadBool m = MonadState Bool m
type Stack = StateT (Int, Int) (StateT Bool IO) ()
ret :: Stack
ret = apply
apply :: (MonadTuple m, MonadBool m) => m ()
apply = undefined
The definition of MonadState
has a functional dependency m -> s
, which means that one monad m
must have at most one instance of MonadState s m
. Or, in plainer terms, the same monad cannot have two instances of MonadState
for two different states, which is exactly what you're trying to do.
There is a simpler solution:
apply :: (MonadTuple (t m), MonadBool m, MonadTrans t) => t m ()
apply = undefined
You can use get
and put
inside apply
to touch the (Int, Int)
state, and lift get
and lift . put
to touch the Bool
state.
However, this requires that StateT (Int, Int)
be the top-level transformer. If it is lower than the top, you need to encode the depth by putting the appropriate number of additional transformers in your type; e.g. if it was the third thing down then you would need
apply :: (MonadTuple (t1 (t2 (t3 m))), MonadBool m, MonadTrans t1, MonadTrans t2, MonadTrans t3) => t1 (t2 (t3 m)) ()
apply = undefined
and would need to use three lift
s for every access to the Bool
state, which quickly gets unwieldy and really loses the charm of mtl-style class-polymorphic programming.
A common alternative style is to expose an API that touches the two states but is not class polymorphic. For example,
type Stack = StateT (Int, Int) (StateT Bool IO)
getTuple :: Stack (Int, Int)
getTuple = get
getBool :: Stack Bool
getBool = lift get
(Similarly you'd add a putTuple
and putBool
.)
I guess with modern extensions you could also consider introducing your own class which does not have the fundep that MonadState
has; e.g.
class MonadState2 s m where
get2 :: m s
put2 :: s -> m ()
You could then use a newtype to give two instances, to be disambiguated by type:
newtype Stack a = Stack (StateT (Int, Int) (StateT Bool IO) a)
instance MonadState2 Bool Stack where
get2 = Stack (lift get)
put2 = Stack . lift . put
instance MonadState2 (Int, Int) Stack where
get2 = Stack get
put2 = Stack . put
Callers would then write e.g. get2 @Bool
or get2 @(Int, Int)
if type inference didn't have enough information to pick which instance to use. But I suspect this would get old real fast.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With