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 lifts 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