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 then you can define the composition of f with g as and means nothing else than .
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:
- After calling
createComposableGetterSetter
with a getter and a setter function as arguments, we get back the actutalcomposableGetterSetter
. - Our
composableGetterSetter
will get atoGetterAndSetter
function, that takes some data as input and returns an object with aget
and aset
method. We return a function, that expects the target data as its only argument. - We construct a GetterSetter object by calling (1) with the target data from (2) and passing the return value to the
toGetterAndSetter
function. - 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 callgetterSetter.get()
to simply retrieve this value) and the targetData (we expect, that the setter will return a new version oftargetData
with its focused value set to the return value fromgetterSetter.get()
). - 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 :)
Top comments (8)
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?
Where is x coming from?
Also about this line.
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.
compose
is a higher order function in both ways: It takes functions (g
andf
) 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 callingg
with the result of callingf
withx
.Your second question:
Yes, this is an array of functions but as parameter spread. Spreading a parameter gives you a so called variadic function. For example:
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 ...
Sorry about that!
And also, thanks for the detailed explanation! I will go back to your 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
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 👍