JavaScript Lenses: A Guide to Efficiently Modifying Immutable Data
Tags: javascript, tutorial, functional
Published: 2023-01-30 16:52:57

The software industry has experienced a paradigm shift towards functional programming in recent years. 

A paradigm shift in programming refers to a change in the way we approach writing code. Contrary to the common belief, a paradigm doesn’t bring new features, it comes with a set of limitations. These limitations, when embraced, can bring benefits of writing better software. 

Here is what I mean:

- Assembly language is a low-level language with no limitations, allowing for unlimited access to memory locations and the ability to manipulate bytes, pointers, and code flows.
- Structural programming introduced the concept of code blocks, with limitations on how we treat iterations and subroutines.
- Object-Oriented Programming (OOP) added further limitations by controlling how internal memory of objects can be accessed through encapsulation and abstraction.
- Functional programming, limits us to the use of pure functions and immutable structures, leading to greater clarity and understanding in the code. This is particularly useful in parallel and concurrent programming, as pure functions and immutable objects minimize the chance of unintended modifications.

Working with immutable structures in functional programming can present challenges, particularly in JavaScript which lacks intrinsic tools for immutability. This requires developers to take responsibility for ensuring immutability. Simple updates, such as changing a value through destructuring, are straightforward:

```tsx
const person = {
  name: 'John',
  age: 25
}

const newPerson = { ...person, age: 26 }
```

Updating an array is similarly simple:

```tsx
const users = [...]

const newUsers = users.concat([newPerson]) // or [...users, newPerson]
```

However, as the complexity increases with nested structures, it becomes more difficult to create a new copy with updated data. For example, updating a nested object like `person.address`:

```tsx
const person = {
  name: 'John',
  address: {
    street: '1 Water Ln',
    city: 'London'
    index: 'NW1 8NX'
  }
}

const newPerson = {
  ...person,
  address: {
    ...person.address,
    index: 'NW1 8NZ'
 }
}
```

Or updating an object within an array:

```tsx
const users = [...]

const newUsers = [
  {
    ...users[0],
    address: {
       ...users[0].address,
       index: 'NW1 8NZ'
  },
  ...users.slice(1)
]
```

The more nested the structures, the greater the challenge in making updates without mutating the original data.

> JavaScript doesn’t support an easy way to modify immutable data. - _maybe YOU thinking_

What is the solution then? Should we discard the concepts of pure functions and immutable structures? Should we avoid nesting objects and arrays? Is it necessary to switch to a different programming language?

The answer to each of these is no. Today, I'd like to introduce a concept that can address this issue. This concept emerged in 2010 with the extension library `Lens` for the `Haskell` programming language.

> Don't be intimidated by `Haskell`, you'll realise the practicality of the Lens concept in a moment - _me_

A `Lens` allows you to focus on a specific part of an immutable data structure, serving as a functional equivalent of a getter and setter. It can be described as a generic class with two functions: a getter and a setter.

```tsx
class Lens<S, A> {
  getter: (S) => A
  setter: (A, S) => S
}
```

Where `S` represents an immutable object and `A` is the focused value within that object. The getter function defines how to view the value, and the setter is a pure function for updating the object and creating a new instance of `S`.

> Why `S` and `A`? I couldn’t find, however, I can speculate that `S` stands for "Source" and `A` stands for "Focus". The letter `F` is often reserved for representing higher-order types, so `A` was chosen as a common placeholder for simple types.
> 

The implementation of lenses in JavaScript can be done using libraries like `Ramda`, which provides functions for constructing lenses on objects and arrays. Ramda's `R.lens` function creates a lens by providing the getter and setter functions, while `R.lensProp` and `R.lensIndex` functions offer shortcuts for commonly used getters and setters.

```tsx
const nameLens = R.lens(/*getter*/R.prop('name'), /*setter*/R.assoc('name'))
// use of shortcuts
const nameLens = R.lensProp('name')
const firstLens = R.lensIndex(0)
```

So now that we have lenses, what can we do with them? Lenses provide us with the ability to retrieve (using `R.view`), modify (using `R.set`), or update (using `R.over`) properties within a structure.

```tsx
const persons = [
  {name: 'John'},
  {name: 'Steve'}
]

const updateName = person =>
  R.over(nameLens, name => name + ' Smith', person)

R.over(firstLens, updateName, persons) // [{"name": "John Smith"}, {"name": "Steve"}]

persons // [{"name": "John"}, {"name": "Steve"}]
```

In the example above, we utilised an intermediate function to update an array's value. But did you know that lenses can be composed? Let's write a helper function to combine two lenses, allowing us to focus on a smaller part of a structure with a single lens.

```tsx
function andThen<S, S1, A>(lens1: Lens[S, S1], lens2: Lens[S1, A]): Lens[S, A] {
  
  const getter = R.pipe(
    R.view(lens1),
    R.view(lens2)
  )

  const setter = (a, s) => 
    R.over(lens1, R.set(lens2, a), s)
  
  return R.lens(getter, setter)
}
```

This is how we can use the function:

```tsx
const nameLens = R.lensProp('name')
const firstLens = R.lensIndex(0)

const nameInFirstLens = andThen(firstLens, nameLens)

const persons = [
  {name: 'John'},
  {name: 'Steve'}
]

R.over(nameInFirstLens, name => name + ' Smith', persons) // [{"name": "John Smith"}, {"name": "Steve"}]
persons // [{"name": "John"}, {"name": "Steve"}]
```

Let's break down how the `andThen` composition works.

We'll start with the getter aspect. We have a top-level lens lens1 that focuses on the internal structure S1 of S and another lens lens2 that focuses on the internal value A of S1.

![Lens composition](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m81s4jsc6lixhh149sct.png)

Here's how the getter is implemented:

```tsx
const getter = R.pipe(
   R.view(lens1),
   R.view(lens2)
 )
```

`R.pipe` runs the functions in sequence, with the output from one function serving as input to the next. In this case, the getter first uses `lens1` to view the value, which gives the internal structure, then uses `lens2` to view the value within that structure.

The setter implementation is more complex:

```tsx
const setter = (a, s) => 
    R.over(lens1, R.set(lens2, a), s)
```

The setter is a function that takes two inputs `(A, S)` and returns `S`. Let's start by examining `R.set(lens2, a)`, which creates an update function `S1 => S1` that updates the value focused by `lens2` to `a`.

The `R.over` function then updates the value `S1` in `S` using `lens1` to focus on it and the update function created by `R.set`. The signature of `R.over` is `<S, S1>(Lens[S, S1>, S1 => S1, S) => S`.

You can simplify lens composition with `R.compose` from Ramda:

```tsx
const nameLens = R.lensProp('name')
const firstLens = R.lensIndex(0)

const nameInFirstLens = R.compose(firstLens, nameLens)
```

> There is a difference in the lens composition approach described with `andThen` and the one with `R.compose`, but the end result is the same and there's no need to worry about it.

For object-specific composition you can use `R.lensPath`:

```tsx
const addressLens = R.lensProp('address')
const zipLens = R.lensProp('zip')

const zipInAddressLens = R.compose(addressLens, zipLens)
const zipInAddressLens2 = R.lensPath(['address', 'zip'])
```

Multiple updates can be composed as well:

```tsx
const nameLens = R.lensProp('name')
const ageLens = R.lensProp('age')
const zipAddressLens = R.lensPath(['address', 'zip'])

const firstLens = R.lensIndex(0)

const nameInFirst = R.compose(firstLens, nameLens)
const ageInFirst = R.compose(firstLens, ageLens)
const zipInFirst = R.compose(firstLens, zipAddressLens)

const persons = [
  {name: 'John', age: 25, address: { zip: 'NW1 8NZ' } },
  {name: 'Steve', age: 35, address: { zip: 'NW1 8NZ' } }
]

R.compose(
  R.set(ageInFirst, 30),
  R.over(nameInFirst, name => name + ' Smith'),
  R.set(zipInFirst, 'NW1 8NX')
)(persons)
/*
  [
    {
        address: {
            zip: "NW1 8NX"
        },
        age: 30,
        name: "John Smith"
    },
    {
        address: {
            zip: "NW1 8NZ"
        },
        age: 35,
        name: "Steve"
    }
]
*/
```

However, this approach duplicates the array three times to update one person. To reduce this, you can create a composed update function for a single person:

```tsx
const updatePerson = R.compose(
  R.set(ageLens, 30),
  R.over(nameLens, name => name + ' Smith'),
  R.set(zipAddressLens, 'NW1 8NX')
)
```

And then update the array using the lens which focuses on specific person:

```tsx
R.over(firstLens, updatePerson, persons)
```

This produces the same result but avoids creating intermediate arrays that are later discarded.

Now you have a tool to immutably update objects in JS of any composition structure without boilerplate. 

If you have questions, leave your comments below!

Follow for more!