Hey,
After doing Haskell eXchange presentation about profunctor optics and talking things with @adamgundry and @dcoutts I decided to fiddle more with these to get full hierarchy going (with help of @phadej blog posts, purescript-profunctor-lenses and mezzolens libraries) and see how these realistically behave. I succeeded and here it is:
type Optic p a b s t = p a b -> p s t
type Iso a b s t =
forall p. Profunctor p => Optic p a b s t
type Lens a b s t =
forall p. Strong p => Optic p a b s t
type Prism a b s t =
forall p. Choice p => Optic p a b s t
type AffineTraversal a b s t =
forall p. (Choice p, Strong p) => Optic p a b s t
-- p = Star
type Traversal a b s t =
forall p. Traversing p => Optic p a b s t
-- p = (->)
type Setter a b s t =
forall p. Mapping p => Optic p a b s t
-- p = Forget. This is needed because without it 're' would turn 'Prism' into
-- 'Getter', but 'Getter' into 'Review', hence re . re /= id.
type PrismaticGetter a s =
forall p. Cochoice p => Optic p a a s s
-- p = Forget, view1 for extraction of exactly one value.
type Getter a s =
forall p. (Bicontravariant p, Cochoice p, Strong p) => Optic p a a s s
-- p = ForgetM, view01 for extraction of at most one value.
type AffineFold a b s t =
forall p. (Bicontravariant p, Choice p, Cochoice p, Strong p) => Optic p a b s t
-- p = Forget (+ Monoid), viewN for extraction of multiple elements and folding
-- them into one with Monoid instance.
type Fold a b s t =
forall p. (Bicontravariant p, Cochoice p, Traversing p) => Optic p a b s t
-- p = Tagged. Same concept as PrismaticGetter for re . re = id.
type LensyReview b t
= forall p. Costrong p => Optic p b b t t
-- p = Tagged, review for reviewing Iso, Prism or LensyReview.
type Review b t =
forall p. (Bifunctor p, Choice p, Costrong p) => Optic p b b t t
Blue lines mean that optics are convertible between each other with re
.
This is somewhat elegant and doesn't require A* equivalents for all optics (as in the lens library) for additional confusion, also folds don't expose internals in the signature, i.e. it's:
foldrOf' :: Fold a b s t -> (a -> r -> r) -> r -> s -> r
not
foldrOf' :: Getting (Dual (Endo (Endo r))) s a -> (a -> r -> r) -> r -> s -> r
This also adds affine versions of Fold and Traversal, thus solving the view
problem, i.e. there are three version of view and it's clear from their signatures what's going on:
-- Helper type. It's Profunctor, Strong, Choice, Cochoice and Bicontravariant
-- (same as Forget).
newtype ForgetM r a b = ForgetM { runForgetM :: a -> Maybe r }
view1 :: Getter a s -> s -> a
view1 optic = runForget (optic (Forget id))
view01 :: AffineFold a b s t -> s -> Maybe a
view01 optic = runForgetM (optic (ForgetM Just))
-- Here Monoid is needed because we use the fact that Forget is Choice.
viewN :: Monoid a => Fold a b s t -> s -> a
viewN optic = runForget (optic (Forget id))
The only problem (apart from exposing dependence on profunctors
)? Abysmal error messages.
I watched Adam's presentation from last year's Haskell eXchange and here we go:
λ> set (to fst)
<interactive>:13:6: error:
• Could not deduce (Bicontravariant p) arising from a use of ‘to’
from the context: Mapping p
bound by a type expected by the context:
Setter a a (a, b) (a, b)
at <interactive>:13:1-12
Possible fix:
add (Bicontravariant p) to the context of
a type expected by the context:
Setter a a (a, b) (a, b)
• In the first argument of ‘set’, namely ‘(to fst)’
In the expression: set (to fst)
In an equation for ‘it’: it = set (to fst)
So at this point I'm fairly convinced that this needs to be gated beyond an abstraction. As there doesn't seem to be much going on with this library, I'd propose to convert it to profunctor encoding (additional benefit is that as it would be internal, we don't need to depend on profunctors library, we can just copy over necessary bits and there aren't that many) since I have written most of the code already, I'd just need to stick all of these signatures into a newtype and rework the subtyping machinery a little bit.
Bonuses:
The implementation of re
. We can take care of all the inversions with one function.
newtype Re p a b s t = Re { unRe :: p t s -> p b a }
instance Profunctor p => Profunctor (Re p a b) where
dimap f g (Re p) = Re (p . dimap g f)
instance Bicontravariant p => Bifunctor (Re p a b) where
bimap f g (Re p) = Re (p . contrabimap g f)
instance Bifunctor p => Bicontravariant (Re p a b) where
contrabimap f g (Re p) = Re (p . bimap g f)
instance Strong p => Costrong (Re p a b) where
unfirst (Re p) = Re (p . first')
unsecond (Re p) = Re (p . second')
instance Costrong p => Strong (Re p a b) where
first' (Re p) = Re (p . unfirst)
second' (Re p) = Re (p . unsecond)
instance Choice p => Cochoice (Re p a b) where
unleft (Re p) = Re (p . left')
unright (Re p) = Re (p . right')
instance Cochoice p => Choice (Re p a b) where
left' (Re p) = Re (p . unleft)
right' (Re p) = Re (p . unright)
-- | Inverts optics, turning around 'Iso' into 'Iso', 'Prism' into
-- 'PrismaticGetter' (and back), 'Lens' into 'LensyReview' (and back) and
-- 'Getter' into 'Review' (and back).
re :: Optic (Re p a b) a b s t -> Optic p t s b a
re optic = unRe (optic (Re id))
We can also easily convert from Van Laarhoven lens to a profunctor one:
-- | Taken from mezzolens library. For more information see
-- https://r6research.livejournal.com/28432.html
data PStore a b t = PStore (b -> t) a
deriving Functor
-- | Convert from a Van Laarhoven lens to a profunctor lens.
vl :: (forall f. Functor f => (a -> f b) -> s -> f t)
-> Lens a b s t
vl l =
dimap ((\(PStore f a) -> (f, a)) . l (PStore id))
(\(f, b) -> f b)
. second' -- p (b -> t, a) (b -> t, b)
Thoughts and comments are welcome.