Skip to content

Instantly share code, notes, and snippets.

@Lev135
Last active October 7, 2023 10:41
Show Gist options
  • Save Lev135/f05b827dae521028afc12d0bf667c8e4 to your computer and use it in GitHub Desktop.
Save Lev135/f05b827dae521028afc12d0bf667c8e4 to your computer and use it in GitHub Desktop.
Writing profunctor optics by the same template

Writing profunctor optics by the same template

All of the profunctor optics kind have the same, very simple, pattern:

type AnOptic p s t a b = p a b -> p s t
type Optic c s t a b = forall p. c p => AnOptic p s t a b

type Iso   s t a b = Optic Profunctor s t a b
type Prism s t a b = Optic Choice     s t a b
type Lens  s t a b = Optic Strong     s t a b

They are soo similar, that I was curious if we can construct them by the same algorithm without knowing anything about profunctor classes. And soon I realized, that in fact we can.

As always I don't expect my thoughts to be something new

The algorithm is very simple. Let's have a look of them applying to one of the simplest optics — Getter (our getter will be a bit more general, then standard one from lens/optics, in fact it will be PrimGetter from the Oleg's gist).

First of all, we like to have an optic Getter s t a b ≅ s -> a. Let's introduce a concrete getter first, i. e. a getter with fixed profunctor type:

newtype GetterP a b s t = GetterP { runGetterP :: s -> a }

type AGetter s t a b = AnOptic (GetterP a b) s t a b

As you can see GetterP definition just encode our request for an optic, isomorphic to an arrow s -> a and the definition of AGetter is straight-forward (there is one non-trivial moment here though: a b in profunctor's definition parameters go before s t).

How can this work? Let's expand the definition of AGetter:

AGetter = GetterP a b a b -> GetterP a b s t
        ≅ (a -> a) -> (s -> a)

The trick here is that GetterP a b a b is trivial, so we can easily define a view function now:

view :: AGetter s t a b -> s -> a
view = runGetterP . ($ GetterP id)

AGetter's constructor can be defined easily too, but we need something more: we need to construct a polymorphic Getter s t a b = Optic c s t a b for some constraint c. This constraint can be easily derived from what we want too:

class GetterC p where
  getterOp :: (s -> a) -> p a b -> p s t

type Getter s t a b = Optic GetterC s t a b

getterOp here is just a Getter constructor (called to in lens):

getter :: (s -> a) -> Getter s t a b
getter = getterOp

All we need now is to provide a GetterC instance for GetterP:

instance GetterC (GetterP u v) where
  getterOp sa (GetterP au) = GetterP (au . sa)

(in fact, here we define how getters will be composed).

And convertors from AGetter to Getter and vise versa:

storeGetter :: Getter s t a b -> AGetter s t a b
storeGetter = id

cloneGetter :: AGetter s t a b -> Getter s t a b
cloneGetter = getter . view

Since storeGetter is just id, we can use any Getter as AGetter (e. g. viewing through it).

Of course, profunctors and classes, obtained by this algorithm are not the simplest ones. But this wasn't a goal: I'd like to achieve uniformness, maybe in a not optimal way. It seems like I've got it: at least this works for getters, setters, lens, prisms, affine traversals and isos, also for indexed getters and effectful getters (and I hope for other kinds of optics too). At the same time it doesn't work for optics that are unable to compose, e. g. for something isomorphic to set function s -> b -> t. That's why this seems to be a reasonable representation of optics.

Upd. In fact we can abstract over this template using type families.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment