DEV Community

Cover image for Lenses: The What and How
Jonas Winzen
Jonas Winzen

Posted on • Updated on

Lenses: The What and How

In this post, I want to show you what lenses in functional programming are, how you could use them, and most importantly: how you could write your own lenses implementation.

TL;DR
Lenses are directly composable accessors. Read on, to learn how they work, and how you could write your own.

I created a little Notebook on Runkit for you, that contains all examples and a second, alternative implementation. So you could play around with that at any time (before-, while- or after- reading this article). See here: https://runkit.com/mister-what/lenses

Intro

Let's start with a description of a problem. Imagine you have the following data structure, that lists employees by their location and position.

const locations = {
  berlin: {
    employees: {
      staff: {
        list: [
          {
            name: "Wiley Moen",
            phone: "688-031-5608",
            id: "cdfa-f2ae"
          },
          {
            name: "Sydni Keebler",
            phone: "129-526-0289",
            id: "e0ec-e480"
          }
        ]
      },
      managers: {
        list: [
          {
            name: "Cecilia Wisoky",
            phone: "148-188-6725",
            id: "9ebf-5a73"
          }
        ]
      },
      students: {
        list: [
          {
            name: "Kirsten Denesik",
            phone: "938-634-9476",
            id: "c816-2234"
          }
        ]
      }
    }
  },
  paris: {
    employees: {
      staff: {
        list: [
          {
            name: "Lucius Herman",
            phone: "264-660-0107",
            id: "c2fc-55da"
          }
        ]
      },
      managers: {
        list: [
          {
            name: "Miss Rickie Smith",
            phone: "734-742-5829",
            id: "2095-69a7"
          }
        ]
      }
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

Accessing data in this structure from different places all over your application gives you a lot of repetition and might lead to hard to find bugs, when the data structure changed (for whatever reason).
So let's explore an alternative approach for this problem: Lenses

Lenses

Lenses are used for accessing and manipulating data in a safe and immutable way. Well the same is true for accessors (getter & setters) on objects, its not fancy and nothing special. What makes lenses really powerful (and really coool) is that they are directly composable. So what does that mean? If you ever had some maths class in your life, you know, that functions can be composed with each other, i.e. you have f: \ X \rightarrow Y,\ g: \ Y \rightarrow Z then you can define the composition of f with g as g \circ f: \ X \rightarrow Z and means nothing else than (g \circ f)(x) = g(f(x)).

So how would we express a composition in Javascript? Simply like that:

function compose(g, f) {
    return function(x) {
        return g(f(x));
    }
}

// or with fat-arrow functions:
const compose = (g, f) => x => g(f(x));
Enter fullscreen mode Exit fullscreen mode

We could define higher orders of composition in three (or more ways):

// recursive version
const compose = (...fns) => x =>
  fns.length
    ? compose(...fns.slice(0, -1))(
        fns[fns.length - 1](x)
      )
    : x;

// iterative version
const composeItr = (...fns) => x => {
  const functions = Array.from(
    fns
  ).reverse();
  /* `reverse` mutates the array,
    so we make a shallow copy of the functions array */
  let result = x;
  for (const f of functions) {
    result = f(result);
  }
  return result;
};

// with Array.prototype.reduce
const composeReduce = (...fns) => x =>
  fns.reduceRight(
    (result, f) => f(result),
    x
  );

// use it!
console.log(
  compose(
    x => `Hello ${x}`,
    x => `${x}!`
  )("World")
); // -> "Hello World!"

Enter fullscreen mode Exit fullscreen mode

We know now how to compose functions. One thing you may have noticed already, is that function composition works best, when the argument and return value of the composed functions are of the same type.

Let's define a composed getter for the students of a location:

const studentsAtLocation = compose(
    (students = {}) => students.list || [],
    (employees = {}) => employees.students,
    (location = {}) => location.employees
  );

const locationWithName = locationName => (
  locations = {}
) => locations[locationName];

const getBerlinStudents = compose(
  studentsAtLocation,
  locationWithName("berlin")
);

const getParisStudents = compose(
  studentsAtLocation,
  locationWithName("paris")
);

console.log(
  getBerlinStudents(locations)
); // [ { name: 'Kirsten Denesik', ... ]

console.log(
  getParisStudents(locations)
); // []
Enter fullscreen mode Exit fullscreen mode

If you are still with me, you might have noticed, that the getter functions are somehow provided in a reverse order. We will resolve this, by using functions that take a getter as argument and return a getter. This pattern (passing a function and returning a function) will allow us to compose basically from getter/setter pairs, by passing a function that takes a value and returns us a getter/setter pair. Let's take a look, how this could look like:

const createComposableGetterSetter = (
  getter, // (1)
  // -- getter(targetData: TargetData): Value
  setter // (4)
  // -- setter(value: Value, targetData: TargetData) => TargetData
) => toGetterAndSetter => targetData => { // (2)
  const getterSetter = toGetterAndSetter(
    getter(targetData)
  ); // (3)
  /**
   * toGetterAndSetter is called with
   * "data" as argument
   * and returns a GetterSetter object:
   * @typedef {
   *  {
   *    get: function(): *,
   *    set: function(newData: *): GetterSetter
   *  }
   * } GetterSetter
   *
   */
  return getterSetter.set(
    setter(
      getterSetter.get(),
      targetData
    )
  ); // (5)
};
Enter fullscreen mode Exit fullscreen mode

Even if this is "just" a two-line function body, it takes some time to understand what's going on here, so I'll explain step by step:

  1. After calling createComposableGetterSetter with a getter and a setter function as arguments, we get back the actutal composableGetterSetter.
  2. Our composableGetterSetter will get a toGetterAndSetter function, that takes some data as input and returns an object with a get and a set method. We return a function, that expects the target data as its only argument.
  3. We construct a GetterSetter object by calling (1) with the target data from (2) and passing the return value to the toGetterAndSetter function.
  4. We use the GetterSetter objects set() method with the return value of calling the setter (4) with the value of the constructed GetterSetter object (we call getterSetter.get() to simply retrieve this value) and the targetData (we expect, that the setter will return a new version of targetData with its focused value set to the return value from getterSetter.get()).
  5. We return the value (which is again a GetterSetter object) that is returned from getterSetter.set(...) in (5).

toGetterAndSetter

We have now defined our createComposableGetterSetter function. We still need to define our toGetterAndSetter function, that we will use, to either just get data from the target or set data on the target. Let's define our toSetAccessors first:

const toSetAccessors = data => ({
  get: () => data,
  set: newData => toSetAccessors(newData)
});
Enter fullscreen mode Exit fullscreen mode

So simple function constructs an object for us, that is used, whenever we want to set data on the target object. Whenever its set method is called with new data, it will create a new instance of itself that holds the new data and returns this instance.

Next the toGetAccessors function:

const toGetAccessors = data => ({
  get: () => data,
  set() {
    return this;
  }
});
Enter fullscreen mode Exit fullscreen mode

A GetAccessor object should only allow to retrieve its data. When trying to set new data, it will simply return its own instance. This makes it impossible to change after creating it.

Using ComposableGetterSetters (Lenses)

We are now going to create three ComposableGetterSetters -- aka lenses -- to see how they work and what is needed to use them for retrieving values or changing the data (in an immutable way).

Creating lenses

We are going to create one lens that focuses on the property "paris", one lens that has focus on the property "employees" and a third one that has focus on the property "students".
We will use default values in getters (to avoid exceptions) and object spread to maintain immutability in setters.

const parisLens = createComposableGetterSetter(
  obj => (obj || {}).paris,
  (value, obj) => ({
    ...obj,
    paris: value
  })
);

const employeesLens = createComposableGetterSetter(
  obj => (obj || {}).employees,
  (value, obj) => ({
    ...obj,
    employees: value
  })
);

const studentsLens = createComposableGetterSetter(
  obj => (obj || {}).students,
  (value, obj) => ({
    ...obj,
    students: value
  })
);
Enter fullscreen mode Exit fullscreen mode

We notice some repetition here, so let's refactor that:

const lensProp = propName =>
  createComposableGetterSetter(
    obj => (obj || {})[propName],
    (value, obj) => ({
      ...obj,
      [propName]: value
    })
  );

// we can now create lenses for props like this:

const parisLens = lensProp("paris");

const employeesLens = lensProp(
  "employees"
);

const studentsLens = lensProp(
  "students"
);

const listLens = lensProp("list"); // needed to get the list of students

Enter fullscreen mode Exit fullscreen mode

We can now start composing (and using) our lenses:

const parisStudentListLens = compose(
  parisLens,
  employeesLens,
  studentsLens,
  listLens
);

const parisStudentList = parisStudentListLens(
  toGetAccessors
)(locations).get();

console.log(parisStudentList);
// -> undefined, since there is no list of students for paris defined.

const locationsWithStudentListForParis = parisStudentListLens(
  _list => toSetAccessors([])
  // ignore current list and replace it with an empty array
)(locations).get();

console.log(
  locationsWithStudentListForParis
);// -> { ..., paris: { employees:{ ..., students: { list: [] } } } }

Enter fullscreen mode Exit fullscreen mode

As this would be very verbose to use, let's define some helpers:

const view = (lens, targetData) =>
  lens(toGetAccessors)(
    targetData
  ).get();

const over = (
  lens,
  overFn /* like the `mapf` callback in `Array.prototype.map(mapf)`.
    i.e.: You get a value and return a new value. */,
  targetData
) =>
  lens(data =>
    toSetAccessors(overFn(data))
  )(targetData).get();

const set = (lens, value, targetData) =>
  over(
    lens,
    () =>
      value /* we use `over` with a `overFn` function, 
        that just returns the value argument */,
    targetData
  );
Enter fullscreen mode Exit fullscreen mode

Let's try to use our helpers:

// using get, set, over:

const locationsWithStudentListForParis = set(
  parisStudentListLens,
  [],
  locations
);

const locationsWithOneStudentInParis = over(
  parisStudentListLens,
  (list = []) => [
    ...list,
    { name: "You", setVia: "Lens" }
  ],
  locations
);

const locationsWithTwoStudentInParis = over(
  parisStudentListLens,
  (list = []) => [
    ...list,
    { name: "Me", setVia: "Lens" }
  ],
  locationsWithOneStudentInParis
);

// logging the results:

console.log(
  view(parisStudentListLens, locations)
); // -> undefined

console.log(
  view(
    parisStudentListLens,
    locationsWithStudentListForParis
  )
); // -> []

console.log(
  view(
    parisStudentListLens,
    locationsWithTwoStudentInParis
  )
); // -> [ { name: 'You', setVia: 'Lens' }, { name: 'Me', setVia: 'Lens' } ]

console.log(
  view(
    parisStudentListLens,
    locationsWithOneStudentInParis
  )
); // -> [ { name: 'Me', setVia: 'Lens' } ]

console.log(
  locationsWithTwoStudentInParis
); // -> ...

Enter fullscreen mode Exit fullscreen mode

This approach makes upating deeply nested immutable data structures a breeze. To make it even simpler, you could define lensIndex(index: number) and lensPath(path: Array<string|number>) lens creator helpers. lensIndex is then being used to focus on array values. lensPath creates a lens that focuses on deeply nested object properties and array indexes, by creating and pre-composing lenses lensProp and lensIndex lenses for you.

More areas of application for lenses

Lenses are perfect for conversions between all kinds of values like currencies, temperature, units (metric units to imperial units and vise versa), sanitizing user input, parsing and stringifying JSON and much more.

Enjoy trying and playing around with lenses (don't miss to check out the Runkit Notebook). If you didn't understand some of my gibberish, please feel free to ask!

I'm happy to answer any questions :)

Discussion (8)

Collapse
kurisutofu profile image
kurisutofu

That was a complicated read ...
I guess I need to study more about javascript before I come back to this article.

How do you call those kind of function?

function compose(g, f) {
    return function(x) {
        return g(f(x));
    }
}

Where is x coming from?

Also about this line.

const compose = (...fns) => x =>

what is fns?
Is it an array of functions?

Collapse
misterwhat profile image
Jonas Winzen Author

This kind of function is a higer order function. Higher order functions are functions, that take a function as parameter or return a function.

function compose(g, f) {
    return function(x) { // returns a function, that expects one parameter
        return g(f(x));
    }
}

compose is a higher order function in both ways: It takes functions (g and f) as parameters and returns a function as result. The returned function is a function, that expects takes a parameter (x) and returns the result of calling g with the result of calling f with x.

Your second question:

const compose = (...fns) => x =>

Yes, this is an array of functions but as parameter spread. Spreading a parameter gives you a so called variadic function. For example:

function example(...args) {
  return `I was called with ${args.length} args: ${args.join(", ")}.`;
}

console.log(example("foo", "bar")); // -> "I was called with 2 args: foo, bar."
console.log(example("a", "b", "c")); // -> "I was called with 3 args: a, b, c."
console.log(example("a", 2, "c")); // -> "I was called with 3 args: a, 2, c."
console.log(example("a", 2, "c", "d")); // -> "I was called with 4 args: a, 2, c, d."

Collapse
kurisutofu profile image
kurisutofu

raaa ... now that you say it, I see the

return function(x)

while when I posted my comment I read it as below in my head ...

function compose(g, f) {
    function(x) { // returns a function, that expects one parameter
        return g(f(x));
    }
}

Sorry about that!
And also, thanks for the detailed explanation! I will go back to your article!

Collapse
denisinvader profile image
Mikhail Panichev

This is very cool! Will try to use it in Vuex store or in the API calls layer. It seems like a good way to avoid exceptions when server response was changed

Collapse
abrahambrookes profile image
Abraham Brookes

Bro. What are you even TALKING about?? What base knowledge am I missing that would make this decipherable for me? Why is this so CRYPTIC!

Collapse
misterwhat profile image
Jonas Winzen Author

Yeah the topic is kind of hard to get. It took me several weeks to finally wrap my head around this topic. Here is another very good article that helped me a lot to understand lenses: medium.com/@dtipson/functional-len...

Collapse
emgodev profile image
Michael

Article might be confusing people because you go from crawling to racing an ostriche.

Collapse
misterwhat profile image
Jonas Winzen Author

Thanks. I'll keep that in mind for my next article 👍