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 bThey 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 bAs 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 bgetterOp here is just a Getter constructor (called to in lens):
getter :: (s -> a) -> Getter s t a b
getter = getterOpAll 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 . viewSince 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.