Skip to the content.

Type of optics, part 2

In the part 1 of this series, we gave a look at Getter, Fold and Getting. In this part, we’ll look at Setter and Setting (also called ASetter).

When we built a getter, we built it from a function s -> a. What’s the fundamental operation of a setter then? It’s a function (a -> b) -> (s -> t). For example, when we have a function increment an integer (+ 1) :: Int -> Int, that function will be a function that applies it to the first element of a pair and returns (Int, a) -> (Int, a) when s and t are a pair.

Then, how can we build a function in a form (a -> f b) -> (s -> f t) from (a -> b) -> (s -> t)? It’s simple! Identity a is isomorphic to a, so we can just use Identity as f. Now, we get (a -> Identity b) -> (s -> Identity t). Let’s call it Setter1.

The function to convert (a -> b) -> (s -> t) to (a -> Identity b) -> (s -> Identity t) just wraps a value in Identity and unwraps a value from Identity.

type Setter1 s t a b = (a -> Identity b) -> (s -> Identity t)

setting1 :: ((a -> b) -> (s -> t)) -> Setter1 s t a b
setting1 map = \afb -> \s ->
  let ab a = runIdentity (afb a)
      st = map ab
      t = st s
   in Identity t

You can extract (a -> b) -> (s -> t) from Setter1 s t a b like this. It’s the well-known over action.

over1 :: Setter1 s t a b -> (a -> b) -> (s -> t)
over1 setter = \ab -> \s ->
  let afb a = Identity (ab a)
      sft = setter afb
      Identity t = sft s
   in t

Let’s make Setter1 more generic like we did with Getter. When you see setting1, you’ll find that we need a function a -> f a an f a -> a. We can use Applicative for the former, but there seems no good candidate for the latter. So let’s define our own.

type Identical2 :: (Type -> Type) -> Constraint
class Identical2 f where
  extract2 :: f a -> a

instance Identical2 Identity where
  extract2 = runIdentity

With Identical2, we can make Setter1 more generic.

type Setter2 s t a b = forall f. (Functor f, Applicative f, Identical2 f) => (a -> f b) -> (s -> f t)

setting2 :: ((a -> b) -> (s -> t)) -> Setter2 s t a b
setting2 map = \afb -> \s ->
  let ab a = extract2 (afb a)
      st = map ab
      t = st s
   in pure t

over action has to use Identity directly, so we’ll give it another type.

type Setting2 s t a b = (a -> Identity b) -> (s -> Identity t)

over2 :: Setter1 s t a b -> (a -> b) -> (s -> t)
over2 setter = \ab -> \s ->
  let afb a = Identity (ab a)
      sft = setter afb
      Identity t = sft s
   in t

The actual Identical class in lens-family has one more constraint Traversable. This is because you can freely define traverse in terms of Identical2 and Applicative.

identicalTraverse :: (Identical2 f, Applicative f, Functor g) => (a -> g b) -> f a -> g (f b)
identicalTraverse agb fa =
  let a = extract2 fa
      gb = agb a
      gfb = fmap pure gb
   in gfb

Even though adding Traversable constraint adds nothing in practice, it’ll make it even clearer that a functor implementing this class is isomorphic to Identity.

You can read a bit more about isomorphism to Identity in Traversable, Lone and Distributive.

By applying this constraint, we’ll get Identical3.

type Identical3 :: (Type -> Type) -> Constraint
class (Applicative f, Traversable f) => Identical3 f where
  extract3 :: f a -> a

instance Identical3 Identity where
  extract3 = runIdentity

type Setter3 s t a b = forall f. Identical3 f => (a -> f b) -> (s -> f t)

setting3 :: ((a -> b) -> (s -> t)) -> Setter3 s t a b
setting3 map = \afb -> \s ->
  let ab a = extract3 (afb a)
      st = map ab
      t = st s
   in pure t

To put them together, we’ll get these types and functions.

type Identical :: (Type -> Type) -> Constraint
class (Applicative f, Traversable f) => Identical f where
  extract :: f a -> a

instance Identical Identity where
  extract = runIdentity

type Setter s t a b = forall f. Identical f => (a -> f b) -> (s -> f t)

type Setting s t a b = (a -> Identity b) -> (s -> Identity t)

setting :: ((a -> b) -> (s -> t)) -> Setter s t a b
setting map afb = pure . map (extract . afb)

over :: Setting s t a b -> (a -> b) -> s -> t
over setter ab = runIdentity . setter (Identity . ab)

You might wonder it was worth generalizing Setter, but it allows us to define an optic that works both as Getter and Setter, and more.