Search code examples
haskellhaskell-lenslenses

Zoom monad stack maintaining context


Time for my weekly lens question;

I have a monad stack:

newtype Action a = Action
  { runAct :: StateT ActionState (ReaderT Hooks IO) a
  } deriving (Functor, Applicative, Monad, MonadState ActionState, MonadReader Hooks, MonadIO)

data ActionState = ActionState
  { _ed :: Editor
  , _asyncs :: [AsyncAction]
  }

I used makeClassy on the Editor type to generate a HasEditor typeclass which my editor lenses depend on.

An Editor has many Buffers; I've defined another monad stack type for an action that acts over a specific buffer (a BufAction); the only difference is that the StateT is over a Buffer:

newtype BufAction a = BufAction
  { runBufAct::StateT Buffer (ReaderT Hooks IO) a
  } deriving (Functor, Applicative, Monad, MonadState Buffer, MonadReader Hooks, MonadIO)

to run a BufAction I use zoom (editor.buffers.ix selected) to zoom the StateT to a specific buffer; but the issue is that now inside the BufAction I can no longer use any lenses that operate over editor or require HasEditor.

Ideally all Actions run inside a BufAction without lifting, while BufActions cannot run inside an Action. In this case BufAction would require the full ActionState, but also a reference to a specific buffer in order to run; whereas Action just requires the ActionState; so BufAction is a more restrictive Monad and Actions should be embeddable in it.

So approximately I want some sort of type like this:

newtype Action a = forall s. HasEditor s => Action
  { runAct :: StateT s (ReaderT Hooks IO) a
  } deriving (Functor, Applicative, Monad, MonadState s, MonadReader Hooks, MonadIO)

However GHC chokes on this; it can't handle existentials and constraints in a newtype;

I switched it to a data type; but then I lose GeneralizedNewtypeDeriving and need to implement all of those deriving clauses manually; which I'd really rather not do.

I've also tried using a type alias; which would mean I don't need to derive typeclasses, but since I also embed Actions in other datatypes I run into errors; for example since I use Action here:

data ActionState = ActionState
{ _ed :: Editor
, _asyncs :: [Async (Action ())]
, _hooks :: Hooks
, _nextHook :: Int
}

I run into:

• Illegal polymorphic type: Action ()
  GHC doesn't yet support impredicative polymorphism
• In the definition of data constructor ‘ActionState’
  In the data type declaration for ‘ActionState’

Taking a different tact; I've also tried implementing a flexible MonadState instance:

instance (HasEditor s, HasBuffer s) => (MonadState s) BufAction where

but get:

• Illegal instance declaration for ‘MonadState s BufAction’
    The coverage condition fails in class ‘MonadState’
      for functional dependency: ‘m -> s’
    Reason: lhs type ‘BufAction’ does not determine rhs type ‘s’
    Un-determined variable: s
• In the instance declaration for ‘(MonadState s) BufAction’

Because MonadState uses functional dependencies...

Really stuck on this one and I could use a hand!

Thanks for taking a look! I really appreciate the help!


Solution

  • It looks like this is what you're trying to do to me. The constraints about what kind of state is acceptable in an Action will be specified on definitions using Action, not action itself. Then you'll be able to use the zoom function from the lens package to focus on different Buffers in your Editor, for example.

    {-# Language TemplateHaskell #-}
    {-# Language GeneralizedNewtypeDeriving #-}
    {-# Language MultiParamTypeClasses #-}
    {-# Language FlexibleInstances #-}
    {-# Language TypeFamilies #-}
    {-# Language UndecidableInstances #-} -- for the Zoomed type instance
    
    module Demo where
    
    import Control.Monad.State
    import Control.Monad.Reader
    import Control.Lens
    
    data Hooks
    
    data SomeState = SomeState
      { _thing1, _thing2 :: Int }
    
    makeLenses ''SomeState
    
    newtype Action s a = Action
      { runAct :: StateT s (ReaderT Hooks IO) a
      } deriving (Functor, Applicative, Monad, MonadState s)
    
    instance Zoom (Action a) (Action s) a s where
      zoom l (Action m) = Action (zoom l m)
    
    type instance Zoomed (Action s) = Zoomed (StateT s (ReaderT Hooks IO))
    
    example :: Action Int a -> Action SomeState a
    example = zoom thing1