If you missed part 1 of this guide, we talked about auto-currying and function composition using Ramda. In part 2, we will be taking a closer look at lenses.

What is a lens?

A lens allows you to “focus” in on nested properties of an object, and perform actions on it, whilst maintaining context of the original object.

Why use lenses?

The real beauty of lenses is that we can update properties of an object, without mutating the original object.

There are 3 types of actions we can perform on an object with our lenses in Ramda.

Getting a property

Setting a property

Call a function on a property

Since getting properties from an object is a simple operation in javascript, most of the power of lenses will come from the setting and transforming properties.

The real beauty of lenses is that we can update properties of an object, without mutating the original object. However, rather than cloning the entire structure, a shallow clone of the relevant target is made, and the new structure will retain pointers to the untouched data structures. More noticeably in larger objects, this will give us better performance, reducing both time and space complexity.

Let’s look deeper into some of the actions we can perform using lenses.

Getting properties

Below, we will create a game object. To get a property from this object, we will need to create a lens, and then pass the lens and the object to view . We can use lensProp , lensPath , and lensIndex to create a lens.

const game = {

name: 'Keep Talking and Nobody Explodes',

genres: ['Puzzle', 'VR'],

publisher: {

name: 'Steel Crate Games',

location: 'Ottawa, Canada'

}

} const name = R.lensProp('name')

R.view(name, game)

> 'Keep Talking and Nobody Explodes' const first = R.lensIndex(0)

R.view(first, game.genres)

> 'Puzzle' const publisherName = R.lensPath(['publisher', 'name'])

R.view(publisherName, game)

> 'Steel Crate Games' // You can also reference indexes with lensPath

const firstGenre = R.lensPath(['genre', 0])

R.view(firstGenre, game)

> 'Puzzle'

There is 1 more method called lens which requires you to pass a getter and a setter function to it. In most cases, you should be able to achieve what you need with one of the other 3 lens functions. Below we have an example of its usage.

const name = R.lens(R.prop('name'), R.assoc('name')); // achieves the same thing

const alsoName = R.lensProp('name')

Setting properties

This is where lenses get really powerful. We use a function called set which takes a lens, a value, and the object to operate on. Example below:

const game = {

name: 'Overcooked',

platforms: ['PS4', 'XB1', 'NS', 'PC'],

publisher: {

name: 'Team 17',

location: 'Wakefield, England'

}

} const firstPlatform = R.lensPath(['platforms', 0]) const newGame = R.set(firstPlatform, 'PS5', game)

> {

name: 'Overcooked',

platforms: ['PS5', 'XB1', 'NS', 'PC'],

publisher: {

name: 'Team 17',

location: 'Wakefield, England'

}

}

set has returned us a new object with the first platform updated from "PS4" to "PS5" . Note that platforms has been shallow cloned, whilst publisher is still a reference to the same object.

game.platforms === newGame.platforms

> false game.publisher === newGame.publisher

> true

Calling functions on properties

With the over method, we can use a function to transform our data. The over method takes a lens, a function, and a data structure.

const person = {

name: 'Bev',

gender: 'female'

} const addMs = (name) => `Ms. ${name}`

const nameLens = R.lensProp('name') R.over(nameLens, addMs, person)

> {

name: 'Ms. Bev',

gender: 'female'

}