DEV Community

Ashutosh
Ashutosh

Posted on

Lenses Pattern in JavaScript

In functional programming, the Lenses pattern offers a solution for handling data manipulation in an immutable way. A Lens essentially serves as a first-class reference to a subpart of some data type. Despite its regular use in languages with built-in support for lenses (e.g. Haskell), a JavaScript developer can still incorporate the lens pattern via libraries or custom implementations.

This blog post will explore the lenses pattern and demonstrate how you can implement it in JavaScript to work with deeply nested paths.

What is a Lens?
A Lens is a functional pattern used to manage immutable data operations. They let us "zoom in", or focus, on a particular part of a data structure (like an object or an array). Every lens consists of two functions: a getter and a setter.

Getter Function: Retrieves a sub-part of the data.

Setter Function: Updates a sub-part of the data in an immutable way.
An important feature of lenses is that they compose, meaning lenses focusing on nested data can be effectively chained to manipulate a required piece of data.

Implementing Lenses in JavaScript
Even though JavaScript doesn't provide built-in support for lenses, we can create a custom lens function to achieve similar functionality. A basic lens function involves creating a getter and setter to retrieve and update the data respectively.

Let’s start simple and create a lens that is not dynamic, meaning it works with a single predetermined path:

function lens(getter, setter) {
    return {
        get: getter,
        set: setter
    };
}
Enter fullscreen mode Exit fullscreen mode

But the real power of lenses comes when we make them handle deeply nested paths. To achieve that, we make our lens function accept a path (an array of keys), and modify our getter and setter to navigate the object using this path:

function lens(path) {
    return {
        get: (object) => path.reduce((obj, key) => obj && obj[key], object),
        set: (value, object) => {
            const setObjectAtKeyPath = (obj, path, value) => {
                if (path.length === 1) {
                    return { ...obj, [path[0]]: value };
                }

                const key = path[0];
                return { ...obj, [key]: setObjectAtKeyPath(obj[key] || {}, path.slice(1), value) };
            };

            return setObjectAtKeyPath(object, path, value);
        },
    };
}
Enter fullscreen mode Exit fullscreen mode

To use this lens, we create a path array indicating the sequence of keys to the desired property, and pass this path to the lens function. The returned lens object provides get and set methods for reading and updating the property:

const person = {
    name: "John Doe",
    address: {
        street: "123 Main St",
        city: "Anytown",
        country: "USA"
    }
};
Enter fullscreen mode Exit fullscreen mode
// Create a lens for the address.street path:
const streetLens = lens(["address", "street"]);

Enter fullscreen mode Exit fullscreen mode
// Get street using the lens:
console.log(streetLens.get(person)); // Outputs: 123 Main St
Enter fullscreen mode Exit fullscreen mode
// Set street using the lens, resulting in a new (immutable) object:

const newPerson = streetLens.set("456 Broadway St", person);

console.log(newPerson); // Outputs the new person object with the updated street

Enter fullscreen mode Exit fullscreen mode

Conclusion
The lenses pattern rewards developers with the ability to maintain immutability while easily accessing and updating deeply nested data structures. The effectiveness and elegance of lenses are in their composability and the simplicity of resulting code. While JavaScript does not support lenses natively, we've seen how to produce a lens like behaviour using array methods and recursion. However, for simplification, this custom implementation does not handle edge cases that established libraries do. Thus, for production-level code, consider using libraries such as Ramda or partial.lenses that offer more comprehensive lens functionalities.

Top comments (0)