`yoctolens`

: the essence of `lens`

Yesterday I whipped up a small twenty-line lens module while working on a zero-dependency library and realising that I needed lenses. I later extracted it to the `yoctolens`

library as a joke (seriously, you should use `microlens`

instead). It dawned on me that I now understand the lens abstraction well enough to explain it, and that `yoctolens`

would be a perfect teaching tool, so here's my attempt at a tutorial^{1}.

To begin, let's examine the core `Lens`

abstraction:

`type Lens s t a b = forall f. Functor f => (a -> f b) -> (s -> f t)`

This says, if you can convert a `a`

to a `b`

, then you can convert an `s`

(containing an `a`

) to a `t`

(containing a `b`

).

To understand why the domains `b`

and `t`

must be in some functor `f`

, consider the following alternative definition, in which we split the two basic lens operations apart and put them in a product:

```
data LensP s t a b = LensP
{ view :: s -> a
, set :: s -> b -> t
}
```

The morphism `lens`

takes a `LensP`

to `Lens`

```
lens :: (s -> a)
-- ^ Get an a from an s
-> (s -> b -> t)
-- ^ Set the component 'a' to a 'b', changing the outer record type
-- to 't'
-> Lens s t a b
-- ^ A lens into component 'a' of record 's'.
-- If you change 'a' to 'b', it would change the
-- record from 's' to 't'
-- In order to produce an 'f t',
lens getter setter = \afb s ->
fmap -- 2. Next, we want to put the 'b' back into the record 's'.
(setter s) -- Lift the pure setter 'b -> t' to a setter inside the
-- 'f' context, 'f b -> f t'
(afb -- 1. Run the setter on the desired field.
(getter s) -- The input is the field 'a' in the record 's'
) -- This produces a 'b' inside the 'f' context
```

I like to think of the functor as a "data context" in which results are computed --- a weaker notion of the "effect contexts" defined by `Applicative`

s and `Monad`

s. By varying the specific `Functor`

used, we can coerce the general lens type into the types of the components of `LensP`

. That is, we can construct (partial) inverses of the `lens`

morphism from `Lens`

to `LensP`

, or rather, morphisms from `Lens`

to the dual of `LensP`

, `LensS`

:

```
data LensS s t a b
= View (s -> a)
| Set (s -> b -> t)
```

The basic idea is this: in order to operate on a record `s`

, we extract the relevant field `a`

into a context `f a`

, perform an operation `a -> b`

in the context functorially, then extract the result using the appropriate projection from the context.

For example, to `view`

a component `a`

, we need a functor `f`

, such that evaluating `f t`

will always produce data of type `a`

, regardless of what `t`

is. So we use the `Const`

functor, which is just a box containing a value, but with a phantom type parameter to keep `fmap`

s from actually touching the value inside.

```
-- Data.Functor.Const
newtype Const c a = Const { getConst :: c }
instance Functor (Const c) where
fmap _ (Const c) = Const c
```

```
view :: Lens s t a b -> (s -> a)
view l = runConst . l Const
```

And to show that it does what you think it does, we calculate:

```
view l
{- definition of 'view l' -}
= runConst . l Const
{- definition of 'l' -}
= runConst . (\afb s -> fmap (setter s) (afb (getter s))) Const
{- beta reduction -}
= runConst . (\s -> fmap (setter s) (Const (getter s)))
{- definition of fmap for Const -}
= runConst . (\s -> (\_ (Const c) -> c) (setter s) (Const (getter s)))
{- beta reduction -}
= runConst . (\s -> (\(Const c) -> Const c) (Const (getter s)))
{- beta reduction -}
= runConst . (\s -> (Const (getter s)))
{- and if you provide input 'record' -}
= getter record
```

In order to `set`

a component of type `a`

to a new value of type `b`

, we require a context which will let us modify the extracted data it contains, i.e. the `Identity`

functor, which is simply a box containing a value.

```
-- Data.Functor.Identity
newtype Identity a = Identity { getIdentity :: a }
instance Functor Identity where
fmap f (Identity a) = Identity (f a)
```

After you extract the `a`

value into `Identity a`

, an `fmap (const b)`

over it will change the context to contain an `Identity b`

. Another fmap with a concrete setter `b -> t`

will pack the new field back into the larger record, returning a `t`

.

```
set :: Lens s t a b -> (b -> s -> t)
set l b = runIdentity . l (const (Identity b))
```

Again, by calculation:

```
set l b
{- definition of 'set l b' -}
= runIdentity . l (const (Identity b))
{- definition of 'l' -}
= runIdentity . (\afb s -> fmap (setter s) (afb (getter s))) (const (Identity b))
{- beta reduction -}
= runIdentity . (\s -> fmap (setter s) ((const (Identity b)) (getter s)))
{- beta reduction -}
= runIdentity . (\s -> fmap (setter s) (Identity b))
{- definition of fmap for Identity -}
= runIdentity . (\s -> (\f (Identity a) -> Identity (f a)) (setter s) (Identity b))
{- beta reduction -}
= runIdentity . (\s -> (\(Identity a) -> Identity ((setter s) a)) (Identity b))
{- beta reduction -}
= runIdentity . (\s -> Identity ((setter s) b))
{- with input 'record' -}
= setter record b
```

I dislike concrete examples, so you should go elsewhere for those.↩