capability icon indicating copy to clipboard operation
capability copied to clipboard

Capability focusing using lens?

Open andrevidela opened this issue 2 years ago • 4 comments

Hi, apologies, this is neither a feature request nor a bug report. But a question that I did not know where else to ask.

Is there a way to derive a capability using a lens into a state? I understand that the Field/HasField and Pos/HasPos capabilities relate to their lens counterpats. But in my case I have a lens

view :: a -> b 
view = ...
update :: a -> b -> a
update = ...

and a capability HasState "stateName" b and I would like to turn it into a HasState "stateName" a using the lens described above. The ideal type signature would be something like focus :: HasState t b m => Lens a b -> (forall m' . HasState t a m' => m' r) -> m r

Is there a way to achieve this?

Intuitively, I would assume zoom achieves that but I do not see what transformer to use or how to even define one.

andrevidela avatar Mar 17 '23 21:03 andrevidela

I've managed to hack this in but I'm not quite sure if this is the way to go:

newtype WithLens m a = WithLens (m a)
  deriving (Functor, Applicative, Monad)

class HasLens super sub | super -> sub where
  view :: super -> sub
  update :: super -> sub -> super

subState :: HasLens super sub => (sub -> (a, sub)) -> (super -> (a, super))
subState f st = let (v, res) = f  (view st) in (v, update st res)

instance (Monoid superState, HasLens superState subState, HasSink tag superState m) => HasSink tag subState (WithLens m) where
    yield_ tag v = WithLens (yield_ tag (update mempty v ))

instance (Monoid superState, HasLens superState subState, HasSource tag superState m) => HasSource tag subState (WithLens m) where
    await_ tag = WithLens (view <$> await_ tag)

instance (Monoid superState, HasLens superState subState, HasState tag superState m) => HasState tag subState (WithLens m) where
    state_ tag f = WithLens (state_ tag (subState f))

focus
    :: forall super sub tag m r
     . Monoid super
    => HasState tag super m
    => HasLens super sub
    => (forall m'. HasState tag sub m' => m' r) -> m r
focus = zoom @tag @WithLens @'[] @sub

Most notably, it requires Monoid super in order to lift from the substate into the superstate in HasSink. This is because we need a map subState -> superState but there is no such thing. However, provided a neutral element, we can use update which will correctly update the neutral superstate with the data from the substate. That somewhat works but feels very hacky. I'm not quite sure if it makes sense to implement HasState without HasSink but if that was possible, it would clear up this problem.

There is also the problem of the functional dependency on super -> sub which limits the use of this API

andrevidela avatar Mar 17 '23 23:03 andrevidela

Hi @andrevidela ,

Have you looked at the Capability.Reflection module? It lets you define a dictionary manually for a capability. It does take a bit of wrapping your head around, but it would avoid having to create an ad hoc type class.

(if you do build a type class of the sort you did, I would advise adding a tag argument to the class, so that you can attach multiple lenses to a field).

aspiwack avatar Mar 21 '23 08:03 aspiwack

Thank you, for the pointer, I'll take a look!

andrevidela avatar Mar 22 '23 05:03 andrevidela

@andrevidela the capability repository contains some examples using the reflection module here. These might help to better understand the approach.

aherrmann avatar Mar 22 '23 08:03 aherrmann