Introduction to optics: lenses and prisms

The problem

This tutorial assumes that our data structures are immutable. To see why we might want to consider optics, we’ll look at a simple example. We’ll define two object types

type Street = { num: number, name: string };
type Address = { city: string, street: Street };
const a1: Address = 
{ city: 'london', street: { num: 23, name: 'high street' } }
const name = a1.street.name
const a2: Address = {
...a1,
street: {
...a1.street,
name: 'main street'
}
}

Lenses

A lens is a first-class reference to a subpart of some data type. Given a lens there are essentially three things you might want to do

  • view the subpart
  • modify the whole by changing the subpart
  • combine this lens with another lens to look even deeper
interface Lens<S, A> {
get(s: S): A,
set(a: A, s: S): S
}
const address: Lens<Address, Street> = {
get: address => address.street,
set: (street, address) => ({ ...address, street })
}
address.get(a1)
// => {num: 23, name: "high street"}
address.set({num: 23, name: 'main street'}, a1)
// => {city: "london", street: {num: 23, name: "main street"}}
const street: Lens<Street, string> = {
get: street => street.name,
set: (name, street) => ({ ...street, name })
}

Composition

The great thing about lenses is that they compose

function composeLens<A, B, C>(ab: Lens<A, B>, bc: Lens<B, C>): Lens<A, C> {

return {
get: a => bc.get(ab.get(a)),
set: (c, a) => ab.set(bc.set(c, ab.get(a)), a)
}
}
const streetName = composeLens(address, street)streetName.get(a1)
// => "high street"
streetName.set('main street', a1)
// => {city: "london", street: {num: 23, name: "main street"}}

Modify

Let’s say we need to set the first character of the address street name in upper case. Mapping a function over a part of a data structure given a lens is as simple as get the value, apply the function to the value, set the new value to be the result

function overLens<S, A>(lens: Lens<S, A>, f: (a: A) => A, s: S): S {
return lens.set(f(lens.get(s)), s)
}
function capitalize(s: string): string {
return s.substring(0, 1).toUpperCase() + s.substring(1)
}
overLens(streetName, capitalize, a1)
// => {city: "london", street: {num: 23, name: "High street"}}

Prisms

In the above example, we used capitalize to upper case the first letter of a string. It works but it would be clearer if we could use Lens to zoom into the first character of a string. However, we cannot write such a Lens because a Lens defines how to focus from an object S into a mandatory object A and in our case, the first character of a string is optional as a string might be empty. For this we need a sort of partial Lens.

interface Prism<S, A> {
get(s: S): ?A,
set(a: A, s: S): S
}
const first: Prism<string, string> = {
get: s => s ? s.substring(0, 1) : null,
set: (a, s) => s.length ? a + s.substring(1) : ''
}
function composePrism<A, B, C>(ab: Prism<A, B>, bc: Prism<B, C>): Prism<A, C> {  return {
get: a => {
const b = ab.get(a)
return b == null ? null : bc.get(b)
},
set: (c, a) => {
const b = ab.get(a)
return b == null ? a : ab.set(bc.set(c, b), a)
}
}
}
function overPrism<S, A>
(prism: Prism<S, A>, f: (a: A) => A, s: S): S {
const a = prism.get(s)
return a ? prism.set(f(a), s) : s
}
function toUpper(s: string): string {
return s.toUpperCase()
}
overPrism(composePrism(streetName, first), toUpper, a1)
// => {city: "london", street: {num: 23, name: "High street"}}

Union types

Prisms are applicable to all union types. How would we write a prism for a custom union type of our own?

type Domicile
= { type: 'office', address: Address }
| { type: 'personal', address: string };
const office: Prism<Domicile, Address> = {
get: d => d.type === 'office' ? d.address : null,
set: (address, d) => d.type === 'office' ?
{ type: 'office', address } : d
}
const d1 = { type: 'office', address: a1 }
const d2 = { type: 'personal', address: '23 high street' }
office.get(d1)
// => {city: "london", street: {num: 23, name: "high street"}}
office.get(d2)
// => null

--

--

mathematician and rock climber

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
gcanti

gcanti

mathematician and rock climber