Lenses provide a means to decouple an object's shape from the logic operating on that object. It accomplishes this using the getter/setter pattern to 'focus in' on a sub-part of the object, which then isolates that sub-part for reads and writes without mutating the object.

If we're using Ramda for lens creation, it only makes sense to take advantage of Ramda functions for other parts of our code. In particular we'll use Ramda's prop() function to replace our getter and the assoc() function to replace our setter.

Transducersprogramming

The result is not too different from using a single lensPath(). In fact, if I had no need for the individual phonesLens and workPhoneLens in other contexts, I'd probably just use a lensPath() instead. However, the nice thing about this approach is that no one lens has the full knowledge of the entire shape of a person. Instead, each lens just keeps track of their own pieces of shape, relieving that responsibility from the next lens in the composition. If we were to, for example, change the property name phones to phoneList, we'd only need to update the lens responsible for that piece of shape (phoneLens) rather than updating multiple lenses that happen to overlap that path.

It's also worth noting that view() is curryable, in that we can configure view() with just the lens and supply the object later. This becomes particularly handy if you want to compose view() with other functions using Ramda's compose(), pipe(), or various other composition functions.

This is also an example of point-free style or tacit programming in that we don't define one or more of the arguments (in this case, person) in our logic. It can be hard to see how this works if you're not used to this style commonly found in FP, but it can make more sense when broken down...

Functional lens glasses

Notice how when applying the lens we have to pass in person.phones. While this works, it's less than ideal because now we're relying on knowledge of the object's shape in our general application code, rather than hiding it in our lens. In addition, when applying the lens with the set() function, we get back the array of phones, not the person. This emphasizes that whatever object you give the lens application, the same is what you get back. The likely next step would be to merge the new array of phones back into the person object. This would, of course, need to be done in a non-mutating way... something that Ramda could handle easily. However, it'd be better to not even have to take that extra step. That leads us to the third specialized lens, lensPath() which is designed for focusing in on nested data.

When used with a lens, we can configure assoc() with just the property name, and let the set() function curry the value and data through.

Again, as with most all Ramda functions, prop() is curryable, allowing us to configure it with a property name and provide the data later:

Now imagine the shape of that object changes such that the firstName and lastName properties are replaced with a single property called name which is itself an object containing properties first and last:

The set() function also takes a lens and an object to apply that lens to, as well as a value to update the focused property. And as mentioned earlier, we get back a copy of the object with the focused property changed. And, just like view(), set() is curryable allowing you first configure it with a lens and value and provide it with data later.

Functionallenses

The prop() function takes a property name and an object, and returns the value of that property name on that object. It works very similarly to our getter function.

And finally, lenses are curryable and composable, which makes them fit in well with the FP paradigm. We'll use both of these in later examples.

Optics functionalprogramming

The view() function takes two arguments; a lens, and an object to apply that lens to. It then executes the len's getter function to return the value of the property the lens is focused on; in this case, firstName.

We'll start with Ramda's most generic lens creation function called simply lens(). As mentioned previously, lenses use the getter/setter pattern for reading and writing data to our object. Let's create those first.

Another benefit to lenses is the ability to write to an object without mutating the object in the process. The non-mutation of data is, of course, one of the staples of FP (functional programming). The problem is, the larger and more complex the data you're working with is, the more difficult it becomes to change deeply nested data without mutations. As we'll see later on, lenses simplify the process with just a couple lines of code no matter how complex your data is.

Any code working with that object would now need to be updated to reflect the object's change in shape. This is prevented in OOP through use of classes which hide the data's internal structure and provides access through a getter/setter API. If the shape of a class's internal data changes, all that needs updated is that class's API. Lenses provide the same benefit for plain old objects.

Decoupling an object's shape allows for future reshaping of your data while minimizing the effects of the rest of the code in your application. Take, for example, an object representing a person.

When passing a single argument to a multiary (multi-arg) curried function, it returns a new function accepting the rest of the arguments. It's not until all the arguments are supplied that it executes its function body and returns the results. So when configuring prop() with just the property name, we'll receive a new function that takes the data argument. That matches perfectly with what a lens getter is: a function that takes a data argument.

With its first international news channel launched in 2005, RT is now a global TV news network providing news, current affairs and documentaries in six languages: English, Arabic, Spanish, French, German and Russian, and sister multimedia news agency RUPTLY that provides livestreaming, video on demand, archive footage and broadcast services. RT creates news with an edge for viewers who want to Question More. RT covers stories overlooked by the mainstream media, provides alternative perspectives on current affairs, and acquaints international audiences with a Russian viewpoint on major global events. RT is the only Russian TV channel to be an eleven-time Emmy finalist. The network has been nominated for International News & Current Affairs Emmy for its coverage of the humanitarian crisis in the Iraqi city of Mosul, Guantanamo Bay inmates’ hunger strike, the Occupy Wall Street protests and other stories. RT also has been shortlisted for News and Documentary Emmy Awards for its VR project ‘Lessons of Auschwitz’, commemorating the 75th anniversary of the liberation of the Auschwitz-Birkenau concentration camp by the Soviet Army. RT America’s ‘Boom Bust’ financial show was a Daytime Emmy finalist for Outstanding Main Title and Graphic Design for a Live Action Program in 2020, while Pulitzer-prize laureate Chris Hedges, host of ‘On Contact with Chris Hedges,’ secured RT America its first Daytime Emmy nomination in 2017, as an Outstanding Informative Talk Show Host.

DEV Community — A constructive and inclusive social network for software developers. With you every step of your journey.

As you can see, lensPath() takes an array with path segments leading to the nested data that we want focused. Each path segment can be a property name or an index. Since we're giving it the root person object, we get back a full copy of the person object with just the home phone number changed. In my opinion, this is where the lens feature really starts to shine. Imagine if we wanted to duplicate the result of the set() function above, but with regular Javascript. Even with the latest features, such as spreading and destructuring, we might end up with something like the following:

The assoc() function works the same way, but is designed for writing rather than reading. In addition, it'll return a copy of the object it's writing to, which is the same functionality required by a lens setter.

There's a third lens application function called over(), which acts just like set() except, instead of providing an updated value, you provide a function for updating the value. The provided function will be passed the result of the lens's getter. Let's say we want to uppercase the person's firstName:

The lens() function takes two arguments, the getter and setter we defined previously. The lens is then ready to be applied to an object, in this example, the person object. But before we do so I want to point out a few things.

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink.

Those are basics of lenses but there are other, more specialized, lens creation functions in Ramda. Specifically, lensProp(), lensIndex(), and lensPath(). These are the functions you'll probably use most often when creating lenses. The generic lens() would be used only when needing to do very customized lens creation. Let's go over each of these specialized lens creation functions.

One of the more powerful features of lenses is their ability to be composed together with other lenses. This allows you to build up new and more complex lenses from existing ones:

The lensIndex() function works similarly to lensProp() except it's designed for focusing in on an array index, and therefore, you pass it an index rather than a property name. Let's add an array of data to our person to test it out.