2

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

duplode
  • 33,731
  • 7
  • 79
  • 150
Chris Penner
  • 1,881
  • 11
  • 15
  • Does your `zoomer` compile? `nextState` is an `s` but `put` should take an `AppState`, I think. – Benjamin Hodgson Mar 01 '17 at 22:46
  • 2
    It seems that you have over-engineered a solution to a fairly straightforward problem. What is wrong with having a single `StateT` containing both the global state and the additional state, e.g. `newtype App s a = App (State (AppState, s) a)`? As for your attempts; in the first, having a `runStateT` in the body of an action is almost always the wrong thing to do, for precisely the reason you stated. In the second, the error you posted does not correspond to any code you posted - the 2nd `App` is perfectly valid (`m :: * -> *` is inferred unless you have something stating otherwise). – user2407038 Mar 01 '17 at 22:54
  • @user2407038 I'm afraid that doesn't work since the `s` of the `StateT` is actually INSIDE `AppState`, hence the zooming. – Chris Penner Mar 01 '17 at 23:19
  • @BenjaminHodgson I believe nextState is an `AppState` actually, since `zoom lns act` zooms the `act` to run over an `App`, which when run produces an `AppState`, which put accepts. Does that make sense? The nesting is confusing :P, hence the question in the first place. – Chris Penner Mar 01 '17 at 23:22
  • @ChrisPenner I'm not sure I understand what you mean by "the s of the `StateT` is inside `AppState`". `StateT s0 (StateT s1)` and `StateT (s0, s1)` are isomorphic types; this is simply by currying: `s0 -> s1 -> (s1, (s0, a))` vs. `(s0, s1) -> (s1, (s0, a))`. – user2407038 Mar 01 '17 at 23:31
  • Yes, those are isomorphic, but type A that we're editing is literally inside type AppState: `newtype AppState = AppState A` In reality it is nested several levels deep, so if we did `(AppState, A)` then there's two copies of the object, the A, and the A inside AppState, which isn't quite right. – Chris Penner Mar 02 '17 at 01:20

2 Answers2

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 ()
Community
  • 1
  • 1
duplode
  • 33,731
  • 7
  • 79
  • 150
  • I'll try this out next chance I get; what would be the implementation of `liftApp :: MonadState s m => App () -> m ()` in this case? – Chris Penner Mar 02 '17 at 01:17
  • @ChrisPenner That type is too general. `App` has `IO` as the base monad, and so if `m` is `State s` (that is, `StateT Identity s`) then `liftApp` is impossible. Cases in which it is feasible include `m ~ StateT App s` (using `lift`, as you already know) and `m ~ StateT s IO` with you having a lens from `s` to `AppState` (`runApp` then `zoom`). In any case, `MonadState` essentially only gives you `get` and `put`, which are not enough for meaningfully implementing your operation. – duplode Mar 02 '17 at 02:35
  • Hrmm, okay. `liftAction` is what I was hoping to get (through implementing zoomer correctly), I should have phrased my question more appropriately (I'm relatively familiar with general zooming at this point), the trouble comes with 'unzooming' so to speak. I've got a few ideas I'll try out though. – Chris Penner Mar 03 '17 at 04:49
  • This got really tricky in mtl, so I've decided to try rebuilding it in a Free Monad, which I've typically had better luck with, I really need the behaviour of `liftAction`. Thanks so much for your help! Here's the new question: http://stackoverflow.com/questions/42589612/zoom-instance-over-free-monad – Chris Penner Mar 03 '17 at 22:36
  • @ChrisPenner After looking at this for a second time, I think I have found a better way to state your troubles here. 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*? My gut feeling is that we can't, though I don't want to settle for a more definite answer without a little more thought. – duplode Mar 04 '17 at 01:08
  • We can think of them as monads with a monadstate over their chosen state, but yes it needs some extra engineering to be able to 'unzoom'. I did have a working implementation over mtl by using phantom types in a monad I defined, but it was quite complex. I'd still really like to get something working yet, if even it's just the free monad version. – Chris Penner Mar 04 '17 at 03:14
0

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

Community
  • 1
  • 1
duplode
  • 33,731
  • 7
  • 79
  • 150