How to zoom a monad transformer?

326 views Asked by At

thanks again for the help!

I'm making extensive use of the E. Kmett's Lens library, to avoid X/Y problems I'll explain a bit of context.

I'm working on an extensible text editor and want to provide extension writers with a monad DSL, an Alteration is a monad transformer stack with a StateT over the Store type, which basically stores the whole text editor. Inside the Store is an Editor which has Buffers. Users can specify an Alteration to act over the whole store, but to simplify things I also provide a BufAction which operates over just a single buffer.

I was planning on implementing this by using a helper called bufDo which runs a BufAction over each Buffer, and a focusDo which runs a BufAction on the 'focused' Buffer. Here's some context:

data Store = Store
  { _event :: [Event]
  , _editor :: E.Editor
  , _extState :: Map TypeRep Ext
  } deriving (Show)

data Editor = Editor {
    _buffers :: [Buffer]
  , _focused :: Int
  , _exiting :: Bool
} deriving Show

data Buffer = Buffer
  { _text :: T.Text
  , _bufExts :: Map TypeRep Ext
  , _attrs :: [IAttr]
  }

newtype Alteration a = Alteration
  { runAlt :: StateT Store IO a
  } deriving (Functor, Applicative, Monad, MonadState Store, MonadIO)

newtype BufAction a = BufAction 
  { runBufAction::StateT Buffer IO a
  } deriving (Functor, Applicative, Monad, MonadState Buffer, MonadIO)

Here's my proposed implementations for bufDo and focusDo:

bufDo :: ???
bufDo = zoom (buffers.traverse)

-- focusedBuf is a Lens' over the focused buffer (I just 'force' the traversal using ^?! in case you're wondering)
focusDo :: ???
focusDo = zoom focusedBuf

This makes sense in my head and gets close to type-checking, but when I try to add a type for them I get a bit confused, ghc suggests a few things and I ended up with this, which is far from elegant:

bufDo :: (Applicative (Zoomed BufAction ()), Zoom BufAction Alteration Buffer Store) => BufAction () -> Alteration ()

focusDo :: (Functor (Zoomed BufAction ()), Zoom BufAction Alteration Buffer Store) => BufAction () -> Alteration ()

Which makes ghc happy for those definitions, but when I try to actually use either of them I get these errors:

    - No instance for (Functor (Zoomed BufAction ()))
    arising from a use of ‘focusDo’

    - No instance for (Applicative (Zoomed BufAction ()))
    arising from a use of ‘bufDo’

Looking around it seems like I may need to specify an instance for Zoom, but I'm not sure quite how to do that.

Anyone have ideas? I'd also love it if you could explain why I need a Zoom instance (if that's the case).

Cheers!

2

There are 2 answers

3
danidiaz On BEST ANSWER

It seems that there is a Zoomed type family that is used to specify what kind of "effect" we will have when we zoom. In some cases, the Zoomed type instance for a monad transformer appears to piggyback on the Zoomed for the underlying monad, for example

type Zoomed (ReaderT * e m) = Zoomed m

Given that Alteration and BufAction are just newtypes over a state transformer, perhaps we could do the same:

{-# language TypeFamilies #-}
{-# language UndecidableInstances #-}
{-# language MultiParamTypeClasses #-}    

type instance Zoomed BufAction = Zoomed (StateT Buffer IO)

Then we must provide the Zoom instance. Zoom is a multi-parameter typeclass and the four parameters seem to be original monad, zoomed out monad, original state, zoomed out state:

instance Zoom BufAction Alteration Buffer Store where
    zoom f (BufAction a) = Alteration (zoom f a)

We just unwrap BufAction, zoom with the underlying monad, and wrap as Alteration.

This basic test typechecks:

foo :: Alteration ()
foo = zoom (editor.buffers.traversed) (return () :: BufAction ())

I believe you could avoid defining the Zoom instance and have a special-purpose zoomBufActionToAlteration function

zoomBufActionToAlteration :: LensLike' (Zoomed (StateT Buffer IO) a) Store Buffer 
                          -> BufAction a 
                          -> Alteration a
zoomBufActionToAlteration f (BufAction a) = Alteration (zoom f a)       

But then if you have a lot of different zoomable things it can be a chore to remember the name of each zoom funcion. That's where the typeclass can help.

2
freestyle On

As additional to the answer @danidiaz.


Basically, you can avoid Zoom instance by this way:

bufDo :: BufAction () -> Alteration ()
bufDo = Alteration . zoom (editor . buffers . traverse) . runBufAction