Function lens

Today, this week’s series on my favorite functions and patterns in functional programming concludes with lens. I will be publishing future installments, so subscribe to our Coding Creativity newsletter for updates! To review the rest of the installments in this compilation, visit the index: functions and patterns.

Lenses are difficult to articulate. More challenging is presenting a strong use-case. Perhaps you’ve already read up on lenses a bit. Perhaps your tireless search for a function lens brings you here.

How does one describe a lens?

  • A functional getter/setter pair

This is usually the first fact one gleans from all the reading. We know that provided some data structure — say, an object — we can:

import R from 'ramda'

// Capture the getter/setter...
const xLens = R.lens(R.prop('x'), R.assoc('x'));

// Use it to view some place in the data...
R.view(xLens, {x: 1, y: 2});            //=> 1
// Use it to set some place in the data...
R.set(xLens, 4, {x: 1, y: 2});          //=> {x: 4, y: 2}
// And use it to apply some function to some place in the data.
R.over(xLens, R.negate, {x: 1, y: 2});  //=> {x: -1, y: 2}

It’s the some place in the data that’s so operative. Why? Because:

  • Lenses describe the where — yet they need not reference any data in particular.
  • They can also describe the shape of data. We can use them as a roadmap for performing complex operations on any valid data structure we pass.

Once more:

  • Lenses exist independently of the data they may be used to transform.

Such enables lenses to be highly declarative. They describe where to focus without any knowledge of what.

Example: increment wages

For my lens example, I’ll employ Partial Lenses, as this is the richest optics library I’ve yet encountered for JavaScript.

Say we’ve some deeply nested wage information spread across various cities:

const wages = {
  byCity: {
    richmond: [{ userId: 1, hourlyWage: 15 }],
    newOrleans: [
      { userId: 1, hourlyWage: 15 },
      { userId: 2, hourlyWage: 21 }
    ]
  }
};

Our goal is to enumerate through wages, increment each of the wages across all cities — without mutating wages.

It is absolutely possible to achieve as much by way of reduce and map. Believe me, we do not want to:

import R from 'ramda'
import {map, reduce} from 'lodash'

const incrementWages = data =>
  reduce(
    data.byCity,
    (acc, curr, key) => ({
      ...acc,
      byCity: {
        ...acc.byCity,
        [key]: map(
          item => ({ ...item, hourlyWage: R.inc(item.hourlyWage) }),
          curr
        )
      }
    }),
    data
  );

Enter lenses. Behold:

import * as L from 'partial.lenses'

const incrementWages = data =>
  L.modify(['byCity', L.children, L.elems, 'hourlyWage'], R.inc, data);

A function call of the variety:

incrementWages(wages)

Will yield:

Object {
  byCity: {
    richmond: [{ userId: 1, hourlyWage: 16 }],
    newOrleans: [
      { userId: 1, hourlyWage: 16 },
      { userId: 2, hourlyWage: 22 }
    ]
  }
};

Further reading

Resources

We're building an AI-powered Product Operations Cloud, leveraging AI in almost every aspect of the software delivery lifecycle. Want to test drive it with us? Join the ProdOps party at ProdOps.ai.