-->

Lift through nested state transformers (mtl)

2019-07-15 06:21发布

问题:

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

回答1:

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 ()


回答2:

Preliminary note: I took the highly unusual step of posting a second answer because I feel my previous one stands on its own well enough, while this one looks at the issue from a quite different angle.

This answer is now a bit moot, given that you have found a solution already, but in any case I feel I should expand on my latest comment here. In it, I had asked:

You say you want "monads over nested states to be able to run actions over the global state as well". Once you have that, though, can we really think of the nested monads as state monads for their specific substate?

I would say that we can't. With the functionality you suggest implemented, perhaps you would formally have some sort of distinct StateT layer for each substate; however, if you can run global state actions in these layers then the lines between them and the overall state are blurred. As far as isolation goes, you might as well work with a monolithic AppState.

I can, however, think of another plausible intepretation of your requirements, which may or may not be relevant for what you are trying to do. Perhaps you want to retain distinct substates for the components of your framework, but have a core state which is shared by them all. Schematically, that might look like this:

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

data Shared = Shared -- etc.

One simple way of wiring that is would be using two StateT layers:

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

Now you can lift actions on the shared state to e.g. StateT A (StateT Shared IO), and then bring that to App with App . zoom stateA. Working with nested StateT layers, however, can be a little awkward. An alternative is bringing Shared into AppState...

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

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

... and then writing lenses that give access to both the substate and the shared state:

data Substate a = Substate
    { _getSubstate :: a
    , _sharedInSub :: Shared
    }
makeLenses ''Substate

-- There are, of course, lots of other ways of spelling these definitions.
subA :: Lens' AppState (Substate A)
subA = lens
    (Substate <$> _stateA <*> _shared) 
    (\app (Substate a s) -> app { _stateA = a, _shared = s })

subB :: Lens' AppState (Substate B)
subB = lens
    (Substate <$> _stateB <*> _shared) 
    (\app (Substate b s) -> app { _stateB = b, _shared = s })

Now you just have to zoom with e.g. subA instead of stateA. There is a little extra boilerplate in having to define these lenses, but that can be alleviated if need be.

Incidentally, there is no combinator in lens that captures this pattern -- for instance, something with type Lens s t a b -> Lens s t c d -> Lens s t (a, c) (b, d) -- because in general it doesn't produce legal lenses -- the lenses have to be disjoint, as ours are, for it to work properly. That said, with a little more shuffling we can express what we are doing in terms of alongside, though I'm not sure if that is in any way advantageous:

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

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

subA :: Lens' (ComponentState, Shared) (A, Shared)
subA = alongside stateA simple

subB :: Lens' (ComponentState, Shared) (B, Shared)
subB = alongside stateB simple

(If you don't like working with the naked pairs, you might define AppState and Substate a isomorphic to them and use the corresponding Isos to submit them to alongside.)