4

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!

Chris Penner
  • 1,881
  • 11
  • 15
  • This is essentially a duplicate of [this question](http://stackoverflow.com/questions/29407289/lens-zooming-newtype). – freestyle Dec 13 '16 at 22:51
  • I saw that one, but was still a little confused, they give a solution, but don't explain it very clearly. – Chris Penner Dec 13 '16 at 22:55

2 Answers2

3

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.

danidiaz
  • 26,936
  • 4
  • 45
  • 95
  • I'll try this out! Thanks! Would you be able to explain the `type instance` line a little more in depth? I haven't learned type families yet :P Also, why do we need undecidable instances? And lastly, why do I need both a type family `Zoomed` and the Zoom instance? I'd love to understand why it all works and why it's all required. Cheers! :) – Chris Penner Dec 13 '16 at 23:20
  • 1
    @Chris Penner Type families are a bit like functions at the type level. They take a type (in our case `BufAction`) and produce another type (in our case `Zoomed (StateT Buffer IO)`). They have some particularities compared to regular functions: the syntax is not the same, the different "cases" can be dispersed in different modules, and so on. You can "run" a type family in `ghci` using the `:kind!` command, like `:kind! Zoomed (StateT Buffer IO)`. – danidiaz Dec 13 '16 at 23:31
  • 1
    @Chris Penner `UndecidableInstances` is required because when dealing with complex instance definitions GHC can be unsure about if typechecking can terminate. Turning on `UndecidableInstances` says "compiler, don't worry about non-terminating typechecking, if it takes too long I will just Ctrl-C and be done with it." It's not a harmful extension, if typechecking actually terminates, of course. – danidiaz Dec 13 '16 at 23:34
3

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
freestyle
  • 3,692
  • 11
  • 21
  • This works great! I appreciate the explanation along with @danidiaz's answer of course, it's nice to learn about type families finally! But is there any reason why I should use the type family wizardry instead of this solution? – Chris Penner Dec 13 '16 at 23:51
  • Yes, it is, if you want to add possibility to use the `zoom` or somthing like the `zoomBufActionToAlteration`. This reason maybe (as wrote @danidiaz) if you have a lot of different zoomable things. – freestyle Dec 14 '16 at 00:04