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. view
ing 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.