Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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
like image 804
uucp Avatar asked Dec 23 '22 08:12

uucp


2 Answers

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.

like image 54
Fyodor Soikin Avatar answered Apr 27 '23 15:04

Fyodor Soikin


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.

like image 29
Daniel Wagner Avatar answered Apr 27 '23 16:04

Daniel Wagner