on

# Typing Optics (3): Isomorphisms and Prisms

This is the 3rd post documenting my tentative to add typings to my focused lens library.

So far, I’ve been able to add type definitions for

- base typeclasses/interfaces (Functor and Applicative)
- Lenses
- Traversals
- Lens & Traversal Composition
- type definition of
`over`

In this post I’ll be tackling Isomorphisms and Prisms

### Isomorphisms

If we look at the typical definition of Isos in Haskell

```
type Iso s t a b = forall p f. (Profunctor p, Functor f) => p a (f b) -> p s (f t)
```

There is a story behind this representation. You can read it in detail in the linked post (but it’s not required to follow the rest).

To simplify, an Iso can be represented in 2 ways: functional & concrete . The concrete representation matches our intuition about Isos, a pair of inversible functions

```
data CIso a s = CIso (s -> a) (a -> s)
-- or using the polymorphic version
data CIso a b s t = CIso (s -> a) (b -> t)
```

The functional representation is similar to Lenses

```
type FIso s t a b = (a -> f b) -> s -> f t
```

We need Isos *to be both above representations at once*. In Haskell we do this by creating a typeclass (something like an interface) that abstracts over both representations and instantiate (implement) the typeclass for each concrete representation. For Isos, the typeclass used is the Profunctor class. By making both functions and `CIso`

instances of it, we can rely on Haskell type inference to choose the appropriate representation (for the interested here is an an implementation example, look at the file Iso.hs).

Now, in TypeScript, I don’t actually want to introduce Profunctors in the library for various reasons. But I can have both representation as part of the same interface.

```
interface Iso<S, T, A, B> {
readonly $type?: "Iso" & "Lens" & "Traversal";
$applyOptic: (<FB, FT>(F: Functor<B, T, FB, FT>, f: Fn<A, FB>, s: S) => FT);
from: (s: S) => A;
to: (b: B) => T;
}
```

Each Iso is a also a Lens (and by extension a Traversal), since the functional implementation is the same as the Lens one.

Now the trick is I can always construct the functional representation given the concrete representation

```
function iso<S, T, A, B>(from: (s: S) => A, to: (b: B) => T): Iso<S, T, A, B> {
return {
$applyOptic(F, f, s) {
return F.map(to, f(from(s)));
},
from,
to
};
}
```

And I can inverse an Iso using the embedded pair of functions

```
function from<S, T, A, B>(anIso: Iso<S, T, A, B>): Iso<B, A, T, S> {
return iso(anIso.to, anIso.from);
}
```

But there is one caveat, if I compose an Iso with another Iso it’ll only give me the composed function `$applyOptic`

, so the original `from`

and `to`

are gone. If I want to preserve the Isomorphism when composing 2 Isos, I’ll have to modify the `compose`

function to handle this special case. In fact, this what’s done in the actual implementation.

Since now we have 3 optic types, normally we’d have to write 9 overloads for `compose`

but as we already saw in the previous post, we need only 3 overloads (since every Iso is a Lens and a Traversal).

```
function compose<S, T, A, B, X, Y>(
parent: Iso<S, T, A, B>,
child: Iso<A, B, X, Y>
): Iso<S, T, X, Y>;
function compose<S, T, A, B, X, Y>(
parent: Lens<S, T, A, B>,
child: Lens<A, B, X, Y>
): Lens<S, T, X, Y>;
function compose<S, T, A, B, X, Y>(
parent: Traversal<S, T, A, B>,
child: Traversal<A, B, X, Y>
): Traversal<S, T, X, Y>;
```

We don’t need to modify the definition of `over`

since it takes the most general type (Traversal).

### Prisms

The definition of Prisms follows the same path. The Haskell definition is

```
type Prism s t a b = forall p f.(Choice p, Applicative f) => p a (f b) -> p s (f t)
```

It’s the same as Isos except we use `Choice`

in place of `Profunctor`

. `Choice`

is also a Profunctor but extends it with additional functions to deal with Sum types like Either (you can read the full story here).

The concrete representation for Prisms is

```
data CPrism s a = CPrism (s -> Either s a) (a -> s)
-- Polymorphic version
data CPrism s t a b = CPrism (s -> Either t a) (b -> t)
```

Again it’s the same as Iso except that instead of `s -> a`

in Isos, we have now a `s -> Either t a`

(while an Iso always succeeds in extracting an `a`

from `s`

, a Prism can fail in which case it returns an alternative `t`

that short-circuits the function `b -> t`

).

Since we need to preserve Prisms over composition, we’ll follow the similar trick we did with Isos.

```
type Either<A, B> = { type: "Left"; value: A } | { type: "Right"; value: B };
interface Prism<S, T, A, B> {
readonly $type?: "Prism" & "Traversal";
$applyOptic: (<FB, FT>(
F: Applicative<B, T, FB, FT>,
f: Fn<A, FB>,
s: S
) => FT);
match: (s: S) => Either<T, A>;
build: (b: B) => T;
}
```

Notice the function takes an Applicative and not a Functor, we’ll see why in a minute.

`prism`

function is used to construct a functional representation from a concrete one

```
function prism<S, T, A, B>(
match: (s: S) => Either<T, A>,
build: (b: B) => T
): Prism<S, T, A, B> {
return {
$applyOptic(F, f, s) {
const eta = match(s);
if (eta.type === "Left") {
// here!!
return F.pure(eta.value);
} else {
return F.map(build, f(eta.value));
}
},
match,
build
};
}
```

`F.pure(eta.value)`

explains why we need an Applicative. In case the `match`

fails in extracting a value from `S`

, we get a `T`

(wrapped in `Either`

), since we need to return an `F<T>`

from `T`

, the `pure`

function from the Applicative interface allows us to wrap plain values into the Applicative context (In fact we don’t need the whole Applicative just the `pure`

part, this restricted interface is sometimes called `Pointed`

).

As for `compose`

we need to add only one overload, if you consulted the implementation of `compose`

linked in the previous section, you’ve already seen that there is also a special case analysis for composing 2 prisms.

```
function compose<S, T, A, B, X, Y>(
parent: Prism<S, T, A, B>,
child: Prism<A, B, X, Y>
): Iso<S, T, X, Y>;
```

So far, it seems we have typings for the 4 optics, Remaining:

- Add typing for accessor functions
`view`

,`preview`

and co - I’ll have to figre Something for Getters
- Proxies