Introduction to Functional Programming in JavaScript: Lenses #9
Tags: javascript
Published: 2024-07-12 22:00:00

Lenses are a powerful and elegant way to focus on and manipulate parts of immutable data structures in functional programming. They provide a mechanism to get and set values within nested objects or arrays without mutating the original data. 

#### What are Lenses?

A lens is a first-class abstraction that provides a way to access and update the parts of a data structure. A lens is typically defined by two functions: a getter and a setter.

- **Getter**: A function that extracts a value from a data structure.
- **Setter**: A function that updates a value within a data structure and returns a new copy of the structure.

Lenses are particularly useful for working with immutable data structures, as they allow for changes to be made without mutating the original data.

#### Benefits of Lenses

1. **Immutability**: Lenses facilitate working with immutable data structures, ensuring that the original data is not modified.
2. **Modularity**: Lenses allow you to modularize data access and updates, making your code more reusable and easier to maintain.
3. **Composability**: Lenses can be composed to focus on nested parts of a data structure, enabling complex data manipulations to be broken down into simpler, composable operations.

#### Implementing Lenses in JavaScript

Let's start with a basic implementation of lenses in JavaScript.

##### Basic Lens Implementation

A lens can be implemented as an object with `get` and `set` methods.

```javascript
const lens = (getter, setter) => ({
  get: (obj) => getter(obj),
  set: (val, obj) => setter(val, obj),
});

const prop = (key) => lens(
  (obj) => obj[key],
  (val, obj) => ({ ...obj, [key]: val })
);

// Usage
const user = { name: 'Alice', age: 30 };

const nameLens = prop('name');

const userName = nameLens.get(user);
console.log(userName); // 'Alice'

const updatedUser = nameLens.set('Bob', user);
console.log(updatedUser); // { name: 'Bob', age: 30 }
```

In this example, `prop` creates a lens that focuses on a specific property of an object. The `get` method retrieves the value of the property, and the `set` method updates the value and returns a new object.

##### Composing Lenses

Lenses can be composed to work with nested data structures. Here, we'll create a utility to compose lenses.

```javascript
const composeLenses = (outerLens, innerLens) => ({
  get: (obj) => innerLens.get(outerLens.get(obj)),
  set: (val, obj) => outerLens.set(innerLens.set(val, outerLens.get(obj)), obj),
});

// Usage with nested data
const addressLens = prop('address');
const cityLens = prop('city');

const userAddressCityLens = composeLenses(addressLens, cityLens);

const user = {
  name: 'Alice',
  address: {
    city: 'Wonderland',
    zip: '12345',
  },
};

const userCity = userAddressCityLens.get(user);
console.log(userCity); // 'Wonderland'

const updatedUser = userAddressCityLens.set('Oz', user);
console.log(updatedUser); // { name: 'Alice', address: { city: 'Oz', zip: '12345' } }
```

In this example, `composeLenses` allows us to create a lens that focuses on the `city` property inside the `address` object. This enables nested property access and updates in a modular and reusable way.

#### Practical Applications of Lenses

Lenses are particularly useful in scenarios where immutability and modular data manipulation are important, such as in state management for front-end applications.

##### Managing State in React

In a React application, lenses can be used to manage state updates in a more functional and predictable manner.

```javascript
import React, { useState } from 'react';

const App = () => {
  const [state, setState] = useState({
    user: {
      name: 'Alice',
      address: {
        city: 'Wonderland',
      },
    },
  });

  const userLens = prop('user');
  const addressLens = prop('address');
  const cityLens = prop('city');

  const userAddressCityLens = composeLenses(userLens, composeLenses(addressLens, cityLens));

  const updateCity = (newCity) => {
    const newState = userAddressCityLens.set(newCity, state);
    setState(newState);
  };

  return (
    <div>
      <p>City: {userAddressCityLens.get(state)}</p>
      <button onClick={() => updateCity('Oz')}>Move to Oz</button>
    </div>
  );
};

export default App;
```

In this example, we use lenses to modularize the access and update of the nested `city` property within the React component's state. This approach makes the state updates more predictable and easier to manage.