5

Here's a more specific variant of this question: Mutate only focus of Store Comonad?, for the benefit of not asking more than one question at once.

Are there any lenses compatible with Control.Lens which allow me to interact with the focus of a comonad (the value from extract) or with the index/value of the Store Comonad (pos)?

It seems like lenses may be of some use here, but I have been unable to find anything which fits; any help would be appreciated, thanks!

Chris Penner
  • 1,881
  • 11
  • 15
  • I think there are two separate questions here: "can I use lenses to work with a `Comonad`'s focus?" and "can I use indexed optics to work with `Representable` functors?" I've answered the first one below, but you should extract the second one into another question. – Benjamin Hodgson Mar 31 '18 at 10:59

1 Answers1

3

Comonad doesn't give you any way to write back to the comonad's focus, so you can't write a general Lens for extract. But it is very easy to turn any old function into a Getter:

extracted :: Comonad w => Getter (w a) a
extracted = to extract

Of course many comonads do allow you to write into the focus - Store, as you mentioned, but also (including but not limited to) Env, Traced and Identity.

-- I suspect some of these laws are redundant:
--   write id = id
--   write f . write g = write (f . g)
--   extract . write f = f . extract
--   duplicate . write f = write (write f) . duplicate
--   write (const (extract w)) w = w
class Comonad w => ComonadWritable w where
    write :: (a -> a) -> w a -> w a

instance ComonadWritable Identity where
    write f (Identity x) = Identity (f x)

-- law-abiding "up to Eq"
instance (Eq s, ComonadWritable w) => ComonadWritable (StoreT s w) where
    write f (StoreT w s) = StoreT (write g w) s
        where
            g k s'
                | s' == s = f (k s')
                | otherwise = k s'

-- law-abiding "up to Eq"
instance (Eq m, Monoid m, ComonadWritable w) => ComonadWritable (TracedT m w) where
    write f (TracedT w) = TracedT (write g w)
        where
            g k m
                | m == mempty = f (k m)
                | otherwise = k m

instance ComonadWritable w => ComonadWritable (EnvT e w) where
    write f (EnvT e w) = EnvT e (write f w)

Given ComonadWritable it's easy to construct a Lens for a comonad's focus.

focus :: ComonadWritable w => Lens' (w a) a
focus = lens extract (\w x -> write (const x) w)

One note on efficiency: StoreT and TracedT's write implementations build a chain of functions with equality checks on the way down, so extract is O(n) in the number of write calls. Since you mentioned you're using a Representable comonad w, you could implement some clever strategy of batching up edits and reifying them into an actual w every so often. Or you could store edits in a Map (strengthening the Eq constraint to Ord) and delegate to the underlying w when it turns out an element hasn't been edited. I'll leave that part to you.

Benjamin Hodgson
  • 42,952
  • 15
  • 108
  • 157
  • This is helpful, but the O(n) is a concern I'm concerned about; I can do much better using the underlying Vectors (from the other question) and composing `ix` lenses; thanks though this is interesting! – Chris Penner Mar 31 '18 at 15:16
  • "I can do much better using the underlying Vectors" - well, when you edit an element of an immutable vector you have to copy the whole thing, so you get O(1) `extract` at the expense of a `write` which is O(n) in the size of the vector. That's why I suggested batching up edits and only building a new `w` every so often, that way you amortise out the costs and get O(1) for both read and write. – Benjamin Hodgson Mar 31 '18 at 15:38
  • @dfeuer The `write (const (extract w)) w = w` law says that `write` targets no more than one `a` in a `w a` (namely the one that `extract` targets). I came up with this based on the get-then-set `Lens` law. – Benjamin Hodgson Mar 31 '18 at 20:24
  • Good question, though. You should undelete your comment! – Benjamin Hodgson Mar 31 '18 at 20:25