It's good time to write tongue-in-cheek posts on Christmas Holidays.
You have probably have heard about Profunctor Optics, there is a paper by Matthew Pickering et. al, and my Glassery post. Functor optics are similar and simple. With profunctors you can define type-changing optics, with bare Functor you can define only so called simple optics.
As I mentioned already, to express type changing optics the OpticP (P for profunctor) type synonym takes five arguments: a profunctor and four type variables:
type OpticP p s t a b = p a b -> p s tBut what if you don't want to change types? When s ~ t and a ~ b, we can simplify the above into Functor optics:
type OpticP' p s a = p a a -> p s s
type Optic f s a = f a -> f sAs with profunctor optics, there are different type-classes constraining variable a -functor, that's how we get lenses, prisms, getters, setters etc. We will explore them below.
Every Haskell programmer is familiar with the Functor type class:
class Functor f where
fmap :: (a -> b) -> f a -> f bWhat might be surprising, in functor optics it's used to define Review (similarly as Bifunctor is used to define Review in profunctor optics, this kind of analogy is visible through all definitions).
type Review s a = forall f. Functor f => Optic f s aWe have upto to create Review and review to use one. upto is simply a fmap, and to define review we use Identity functor: a -> b ≅ Identity a -> Identity b.
upto :: (a -> s) -> Review s a
upto = fmap
type AReview s a = Optic Identity s a
review :: AReview s a -> a -> s
review l a = runIdentity (l (Identity a))By the way, A Representation Theorem for Second-Order Functionals by Mauro Jaskelioff and Russell O'Connor tells us that
forall f. Functor f => f a -> f s ≅ a -> swhich is intuitively true. The left to right is witnessed by review, and other direction by upto. However, the paper has more rigorous and general proof.
Let's continue with classes more or less widely used, i.e. there is a package on Hackage!
"Dually" to Functor (dual of Functor is Functor), the Contravariant type class gives rise to a Getter.
Whereas in Haskell, one can think of a
Functoras containing or producing values, a contravariant functor,Contravariantis a functor that can be thought of as consuming values.
Contravariant lives in a contravariant package.
class Contravariant f where
contramap :: (b -> a) -> f a -> f bAs with Review, constructor of Getter is simply a member of the class, in this case contramap:
type Getter s a = forall f. Contravariant f => Optic f s a
to :: (s -> a) -> Getter s a
to = contramapto was easy to define, for view we will need a new newtype Flipped:
newtype Flipped a b = Flip { unflip :: b -> a }
instance Contravariant (Flipped a) where
contramap f (Flip g) = Flip (g . f)
type AGetter s a = Optic (Flipped a) s a
view :: AGetter s a -> s -> a
view l s = unflip (l (Flip id)) sThe Invariant class is less known. It's a moral superclass of both, Functor and Contravariant type classes. In fact, any * -> * type parametric (i.e. ADTs) in the argument permits an instance of Invariant. It lives in invariant package.
class Invariant f where
invmap :: (a -> b) -> (b -> a) -> f a -> f bFor example, Flipped is also an Invariant functor:
instance Invariant (Flipped a) where
invmap _ = contramapReader could guess that invmap is a constructor for Iso.
type Iso s a = forall f. Invariant f => Optic f s a
iso :: (a -> s) -> (s -> a) -> Iso s a
iso = invmapview and review work with optics constructed with iso, e.g.
negated :: Num a => Iso a a
negated = iso negate negate
-- evaluates to -2
example1 :: Int
example1 = view negated 2One remark about Invariant. In Profunctor Optics: The Categorical view Bartosz Milewski looks into profunctor optics. In very similar way, we can look into Functor optics, yet instead of using pairs <a, b> and <s, t>, it's enought to use a and s, then we arrive to the similar conclusion:
forall f. Invariant f => f a -> f s ≅ (s -> a, a -> s)If we don't constraint f, we have an equality. Note: data a :~: b is a non-parametric GADT, and therefore not even an Invariant.
type Equality s a = forall f. Optic f s a
simple :: Equality a a
simple = id
toEquality :: a :~: s -> Equality s a
toEquality = gcastWith
fromEquality :: Equality s a -> a :~: s
fromEquality l = l ReflAlso note, here we havee a Leibnizian equality as in eq package, just without a newtype wrapper:
data a := b = Refl' { subst :: forall c. c a -> c b }So far we handled the edges of optics lattice: Identity, Iso, Getter, and Review. But how about simple Lens?
We can define over using a Endo newtype.
type ASetter s a = Optic Endo s a
over:: ASetter s a -> (a -> a) -> (s -> s)
over l f = appEndo (l (Endo f))Note: Endo is neither Functor, nor Contravariant, but it's Invariant!
-- evaluates to -25
example2 :: Int
example2 = over negated (\x -> x * x) (- 5) But what's the type-class for Setter? Here, we can use what we know about profunctor optics. There, the class is Mapping, we can have similar class too
class Invariant f => Mapping f where
map' :: Functor g => f a -> f (g a)Endo is an instance of this class, but Flipped isn't (we cannot view through a Setter!):
instance Mapping Endo where
map' (Endo f) = Endo (fmap f)So we can defined Setter as
type Setter s a = forall f. Mapping f => Optic f s aFor setting we need an auxiliary Functor Context:
setting :: ((a -> a) -> s -> s) -> Setter s a
setting f = invmap (\(Context g s) -> f g s) (Context id) . map'
data Context a b t = Context (b -> t) a deriving FunctorSetter was defined in a very same way as it would be in a profunctor optics, so are many other optics, e.g. Lens we cover next.
We'll conclude the journey by defining Lens. As with Setter, we can piggyback on what we know from profunctors:
class Invariant f => Strong f where
first' :: f a -> f (a, c)
first' = invmap swap swap . second'
second' :: f a -> f (c, a)
second' = invmap swap swap . first'
{-# MINIMAL first' | second' #-}There aren't that many Strong functors, Endo and Flipped are (we we can over / set and view through the lens, respectively):
instance Strong Endo where
first' (Endo f) = Endo (first f)
instance Strong (Flipped a) where
first' (Flip f) = Flip (f . fst)So finally we can define Lens type-alias and lens constructor:
type Lens s a = forall f. Strong f => Optic f s a
lens :: (s -> a) -> (s -> a -> s) -> Lens s a
lens getter setter fa = invmap
(\(a, s) -> setter s a)
(\s -> (getter s, s))
(first' fa)In this post we saw definitions Functor optics, which are simple versions of Profunctor optics. This simplicity might be useful when experimenting with optics, as there is less type-variables to deal with. Working out Prism or Traversal is this "formalism" is left as an exercise to the reader. Another interesting exercise is to figure out Free Strong, apply it in the construction of Jaskelioff & O'Connor & Bartosz, turn the category theory wheels and see data ConcreteLens s a = CL (s -> a) (s -> a -> s) pop out.
Please comment on Reddit thread or on Twitter.
Edward Kmett mentions on Twitter
Fun fact: We figured out prisms, etc. for this form of optic before we figured out the profunctor versions.
Phil Freeman wonders on Reddit on how you go between Profunctor and Functor optics.
Let's see a simple example converting Lens. From functor to profunctor is simple using Trace newtype Phil mentions:
data Trace p a = Trace { getTrace :: p a a }
instance P.Profunctor p => Invariant (Trace p) where
invmap f g (Trace x) = Trace (P.dimap g f x)
instance P.Strong p => Strong (Trace p) where
first' (Trace x) = Trace (P.first' x)
profunctorFirst :: P.Strong p => OpticP' p (a, c) a
profunctorFirst = getTrace . first' . TraceOther way around is a little tricker!
Phil thinks using existential Split would work. And he is right.
data Split f a b where
Split :: (a -> x) -> (x -> b) -> f x -> Split f a b
instance P.Profunctor (Split f) where
dimap f g (Split h i x) = Split (h . f) (g . i) x
split :: f a -> Split f a a
split = Split id id
unsplit :: Invariant f => Split f a a -> f a
unsplit (Split h i x) = invmap i h xWe can even write Strong instance:
instance Strong f => P.Strong (Split f) where
first' (Split h i x) = Split (first h) (first i) (first' x)So we can convert Profunctor Lens into Functor one:
functorFirst :: Lens (a, c) a
functorFirst = unsplit . P.first' . splitThis direction, using Split, isn't as elegant as other one using Trace, but seems to work!