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.