So it occurred to me today, as I was explaining an example of how to use Ramda Lenses that the value that lenses provide is something that is simple, but not so immediately obvious. A lot of people do not understand why you would use lenses in a business application, and I have had trouble explaining why you might want to in my own dealings with other coworkers, over something like plain addressing or encapsulation in an object.

Put succinctly, lenses are a concept that abstract away data structure addressing, access, and modification. This is dry, so I will attempt to put it in pragmatic terms. The power that lenses bring to a project is the ability to refer to addressing logic in a generic way that allows for the replacement of the underlying data structure without modification to any consumer code.

An example is in order.

Let’s say you have a person represented in a fairly obscure structure, maybe a list:

const personList = ["John Smith", 25, "123 Flower Ln", "Dallas", "Texas", "75204"];

And now you want to get the state, well that’s easy, just reach for the 4th element:

console.log(`proc list: ${personList[4]}`);

Now your boss comes to you and says, good news! We can represent a person as an object now!

You look over the specification of person now, and it looks more like the following:

const personObject = {
  name: "John Smith",
  age: 25,
  address: {
    street: "123 Flower Ln",
    city: "Dallas",
    state: "Texas",
    zip: "75204"
  }
}

So you find and replace all instances of your list index with the following:

console.log(`proc object: ${personObject.address.state}`);

A few months pass, the team has been infiltrated by elitist functional purists who insist on using immutable structures; or maybe they just want full blown maps in ES5…who knows.  Your boss now apprises you to the fact that the person objects look like this:

const personMap = Immutable.Map()
  .set("name", "John Smith")
  .set("age", 25)
  .set("address", Immutable.Map()
        .set("street", "123 Flower Ln")
        .set("city", "Dallas")
        .set("state", "Texas")
        .set("zip", "75204"))

So you once again try to find and replace all your instances like the following:

console.log(`proc map: ${personMap.get("address").get("state")}`);

Exhausted from the change, and irritated at the edge cases where you missed a few consumers, you set out to refactor the code. Trusty object encapsulation to the rescue:

////// object approach

//// person encapsulating list
class Person {
  constructor(person) {
    this.person = person;
  }

  state() {
    return this.person[4];
  }
}

// modified to

//// person encapsulating object
class Person {
  constructor(person) {
    this.person = person;
  }

  state() {
    return this.person.address.state;
  }
}

// modified to

//// person encapsulating immutable map
class Person {
  constructor(person) {
    this.person = person;
  }

  state() {
    return this.person.get("address").get("state");
  }
}

Now your consumer code looks uniform:

console.log(new Person(underlying).state());

But did you need to immediately jump to encapsulation? Could you be introducing incidental complexity by using objects over the underlying data structures? Potentially, when it comes to mutation especially, objects can hide mutation, and you know your elitist functional overlords will never go for that…what to do?

This is where lenses come into play.  Lets take a look at the two lenses that have helpers first:

////// function lens approach

//// index lens
const stateLens = R.lensIndex(4);
const person = personList;

// modified to

//// object lens
const stateLens = R.lensPath(["address", "state"])
const person = personObject;

But what do we do for the case when we want to use a more exotic underlying structure? We construct our own lens:

//// exotic lens
const immutableGet = R.invoker(1, 'get');
const immutableSet = R.invoker(2, 'set');
const immutableAddressLens = R.lens(immutableGet('address'), immutableSet('address'));
const immutableStateLens = R.lens(immutableGet('state'), immutableSet('state'));
const stateLens = R.compose(immutableAddressLens, immutableStateLens);
const person = personMap;

And now our consumer code looks uniform:

console.log(R.view(stateLens, person));

Both objects and lenses provide the ability to abstract over data access. Unlike objects, lenses come with three useful functions: view (allowing you to see the value), set (allowing you to change the value immutably), and over (view, apply function, set).  These three operations are most of the boilerplate that is abstracted over in classical objects, but you can get them for free with lenses.

Additionally, if you’ll notice, the lenses were built piecemeal and then composed together using regular function composition. This is an important additional benefit. Lenses (of the type in Ramda), are just functions, and they compose nicely with any other functions. The lensPath helper in the previous block abstracts over even this aspect. It could have been constructed using composition and Ramda’s lensProp function.

To revisit – lenses abstract data addressing, access, and modification in a uniform (non-idiosyncratic or boilerplate) way providing uniform consumer code and the ability to change the underlying structure without modifying consumer code, without the headache or boilerplate of classes.

All functioning code in JSBin


Brandon Keown