uucp
uucp

Reputation: 429

Haskell -- Chaining two states using StateT monad transformers

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

Upvotes: 4

Views: 1110

Answers (2)

Daniel Wagner
Daniel Wagner

Reputation: 152682

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.

Upvotes: 3

Fyodor Soikin
Fyodor Soikin

Reputation: 80714

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.

Upvotes: 7

Related Questions