Lenses Pattern in JavaScript
Ashutosh
Posted on May 25, 2024
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
};
}
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);
},
};
}
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"
}
};
// Create a lens for the address.street path:
const streetLens = lens(["address", "street"]);
// Get street using the lens:
console.log(streetLens.get(person)); // Outputs: 123 Main St
// 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
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.
Posted on May 25, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.