loading...
Cover image for Lenses: The What and How

Lenses: The What and How

misterwhat profile image Jonas Winzen Updated on ・6 min read

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"
          }
        ]
      }
    }
  }
};

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));

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!"

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)
); // []

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)
};

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)
});

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;
  }
});

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
  })
);

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

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: [] } } } }

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
  );

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
); // -> ...

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 :)

Posted on May 27 '19 by:

misterwhat profile

Jonas Winzen

@misterwhat

Software Engineer at IBM Security. Full-Stack JavaScript dev, with passion for front-end development. Psytrance DJ on weekends, playing in local clubs (yeah, that means: Goa Parties!) :D

Discussion

markdown guide
 

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?

 

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."

 

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!

 

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!

 

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...

 

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

 

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

 

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