Search code examples
haskellmonad-transformershaskell-lensstate-monadhigher-kinded-types

Lift through nested state transformers (mtl)


So I'm working on an extensible application framework, and a key part of the framework is to be able to run state monads over many different state types; I've got it set up, and can run the nested state monads; however a key feature I need is for monads over nested states to be able to run actions over the global state as well; I managed to rig this up using some complicated Free Monads in an earlier project, but now I'm using mtl and I'm a bit stuck.

Here's some context:

newtype App a = App
  { runApp :: StateT AppState IO a
  } deriving (Functor, Applicative, Monad, MonadState AppState, MonadIO)

data AppState = AppState
  { _stateA :: A
  , _stateB :: B
  }
makeLenses ''AppState

I'm trying to define something like:

liftApp :: MonadTrans m => App a -> m App a
liftApp = lift

This works fine of course because of the properties of MonadTrans, but the trick now is when I have an action like this:

appAction :: App ()
appAction = ...

type ActionA a = StateT A App a
doStuffA :: ActionA ()
doStuffA = do
  thing1
  thing2
  liftApp appAction
  ...

This compiles within my app; but the trick comes when run this in an App itself:

myApp :: App ()
myApp = do
  ...
  zoomer stateA doStuffA

I'm having trouble writing zoomer; Here's an attempt:

zoomer :: Lens' AppState s -> StateT s App r -> App r
zoomer lns act = do
  s <- get
  (r, nextState) <- runStateT (zoom lns act) s
  put nextState
  return r

The problem is that runStateT (zoom lns act) s is itself an App, but it also yields an AppState which I then need to put to get the changes. This means that any changes caused in the Monadic part of <- runStateT are overwritten by put nextState.

I'm pretty sure I'm not supposed to be nesting two sets of MonadState AppState like this, but I'm not sure how to get it working since mtl doesn't allow me to nest multiple MonadState's due to functional dependencies.

I also started trying something like inverting it and having App be the outer transformer:

newtype App m a = App
  { runApp :: StateT AppState m a
  } deriving (Functor, Applicative, Monad, MonadState AppState, MonadIO, MonadTrans)

with the hopes of using the MonadTrans to allow:

liftApp = lift

But GHC doesn't allow this:

• Expected kind ‘* -> *’, but ‘m’ has kind ‘*’
• In the second argument of ‘StateT’, namely ‘m’
  In the type ‘StateT AppState m a’
  In the definition of data constructor ‘App’

And I'm not sure that would work anyways...

So that's the issue, I want to be able to nest App monads inside arbitrary levels of StateT's somehow being run inside an App.

Any ideas? Thanks for your time!!


Solution

  • Along the lines of user2407038's comment, this type...

    type ActionA a = StateT A App a
    

    ... looks a bit strange to me. If you want to use zoom stateA, then the other state is a part of AppState. Assuming you can modify the A substate without touching the rest of the AppState (otherwise you wouldn't want zoom in the first place), you should be able to simply define, for instance...

    doStuffA :: StateT A IO ()
    

    ... and then bring that to App with:

    zoomer :: Lens' AppState s -> StateT s IO r -> App r
    zoomer l a = App (zoom l a)
    
    GHCi> :t zoomer stateA doStuffA 
    zoomer stateA doStuffA :: App ()
    

    If you'd rather have a pure doStuffA...

    pureDoStuffA :: State A ()
    

    ... you just have to slip in a return into IO in the appopriate place...

    GHCi> :t zoomer stateA (StateT $ return . runState pureDoStuffA)
    zoomer stateA (StateT $ return . runState pureDoStuffA) :: App ()
    

    ... or, using mmorph for a cuter spelling:

    GHCi> :t zoomer stateA (hoist generalize pureDoStuffA)
    zoomer stateA (hoist generalize pureDoStuffA) :: App ()