I can see clearly now

Using functional lenses to modify objects by example.

Consider object property access. We mostly think of this as a built-in JavaScript syntax. Reading a value at a given property "foo" happens as soon as we write

1
O.foo // value of property "foo" in object "O"

Similarly, as soon as we write the assignment operator we set the value of property "foo"

1
2
O.foo = "bar"
// object "O" has property "foo" with value "bar"

What if we wanted to split reading property "foo" into two steps. Step 1 would just configure it, and step 2 would actually read it? We could easily do this using a closure

1
const read = property => object => object[property]

The function returning a function - that's a common approach. The first call only specifies the property name we plan to access and returns a function that is waiting for an actual object. Interesting that we could call read once and then use the returned function multiple times, even on different objects.

1
2
3
4
const read = property => object => object[property]
const fooReader = read('foo')
fooReader({foo: 42}) // 42
fooReader({bar: 'baz'}) // undefined

We could make a "writer" similarly, and we could even curry the function to accept property, value and object in three separate calls

1
2
3
4
5
6
const write = property => value => object => object[property] = value
const fooSet = write('foo')
const fooSet5 = fooSet(5)
const o = {}
fooSet5(o)
// o is {foo: 5}

We could make write immutable to make sure we are not modifying the object when setting a property, and we could also support deep paths.

1
2
3
const fooBarBazSet = write('foo.bar.baz')
const o = ...
const updated = fooBarBazSet(5, o)

Not only we do not know which object we are going to query or update; we do not even know if we are going to read or write value at a given path like foo.bar.baz. Thus the first call should just "save" the path, and then a separate function call should know how to read or write.

Lenses

Wrapping a path like foo.bar.baz so that in the future we can read from that location in some object, or set its value is called creating a "lens". In Ramda world, it works like this

1
2
3
4
5
6
7
8
9
10
11
12
13
const fooBarBazLens = R.lensPath(['foo', 'bar', 'baz'])

// reading value is called `R.view`
const readFooBarBaz = R.view(fooBarBazLens)
readFooBarBaz({foo: {bar: {baz: 42}}})
//=> 42

// writing value is called `R.set`
const setFooBarBaz = R.set(readFooBarBaz)
const o = {}
const output = setFooBarBaz(1, o)
// o is still {}
// output is {foo: {bar: {baz: 1}}}

Hmm, I wish writing a value using a path inside lens was called "R.project" 😃. But that name is already taken.

If we deal with single level (a plain property), we have function R.lensProp

1
2
const fooLens = R.lensProp('foo')
R.view(fooLens, {foo: 42}) // 42

which is equivalent of R.lensPath(['foo'])

Lenses as street directions

Here is an analogy that helps visualize the lens concept. Imagine giving street directions like "north 2 blocks, east 1 block then north 4 blocks". Directions like this could be used to deliver a box or pick up a box (write and read property value respectively). And directions like this would work (usually) in any intersection city; Manhattan in New York City is a good place.

Directions in NYC

We can apply same directions "N 2, E 1, N 4", and if we start at different places, we will end up in 3 different places.

  • starting at 17th st and 10th ave we will arrive at 23 st and 9th avenue.
  • from 14th and 8th we get to 20th and 7th
  • from 11th and 6th we get to 17th and 5th

Using same directions we can pick up a box or drop it off.

Delivering and picking up values using directions

That's how I think of lenses.

Lenses for the win

Here is a cool use for lenses. I hate programming. Well, not really hate the programming itself as much as I hate the boring code writing. For example, we often take some objects and transform them into other objects; usually picking only some properties, trimming strings etc. Libraries like Ramda and Lodash make it simpler with its variety of functions to modify objects.

Check out things like R.pick and R.evolve - they make object transform quick, yet still this is rote work 👎.

1
2
3
4
5
6
7
8
9
10
11
12
var tomato = {
firstName: ' Tomato ',
data: {elapsed: 100, remaining: 1400},
id:123
}
var transformations = {
firstName: R.trim,
lastName: R.trim, // Will not get invoked.
data: {elapsed: R.add(1), remaining: R.add(-1)}
}
R.pick(['firstName'], R.evolve(transformations, tomato))
//=> {firstName: 'Tomato'}

We often have the initial object, and if we write the above code a bot can quickly evaluate it and give us the output. This is often used to play with code snippets on Ramda Gitter channel

Rambot in action

This has a huge huge shortcoming, actually 2 of them.

  1. We usually have the input object and the desired output object, but not the code. We have to write the code.
  2. Writing code is freaking complicated and requires skills and experience. Who has time for that?

What if we could just give input and output objects and let a bot / CLI / program compute the transformation code?

1
2
3
4
5
6
7
var tomato = {
firstName: ' Tomato ',
data: {elapsed: 100, remaining: 1400},
id:123
}
var desired = {firstName: 'Tomato'}
var t = verySmartBot(tomato, desired)

The computed function t is a transformation function that not only computes t(tomato) to be desired, but applies same transformation to any object with the same shape as input. For example

1
2
3
4
5
6
7
var banana = {
firstName: ' Banana',
data: 'some data',
id: 42
}
t(banana)
// {firstName: 'Banana'}

Cool cool cool, so now anyone can "program" by showing the program "before" and "after" objects, how do we write this "verySmartBot" program and what do lenses have to do with it?

Well, take a look at every property in the output object, like firstName. We can brute force search for same value in the input object, and will file matching property firstName (value is important, not the property name itself). So we find that our transformation needs to

  • read property firstName from the source object (any future source object)
  • transform it using R.trim
  • write value into property firstName of the destination object

Hmm, does this look familiar? Reading source property while deferring the actual object is viewing via a lens R.view(R.lensProp('firstName')). Writing a value into an object is setting via a lens R.set(R.lensProp('firstName')). And in the middle there is R.trim(value)!

Take a look a the implementation in change-by-example. The search for each destination property grabs all paths in the object, including deep properties

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const allPaths = (obj) => ...
const paths = allPaths(destination)
// paths = [['firstName'], ['data'], ['id']]
paths.forEach(path => {
const destinationValue = R.view(R.lensPath(path), destination)
// find source property
const readTransform = findTransformTo(destinationValue)
if (readTransform) {
// found!
const write = R.set(R.lensPath(path))
const readAndWrite = (from, to) => write(readTransform(from), to)
destinationPropertyTransforms.push(readAndWrite)
}
})

You can see how simple the code is yourself here

Similarly, to find the source property (even if deep inside the source object), we use allPaths and lenses for each path.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// find lens inside the source object
// and transform to produce given value
let sourceLens
let transform
const paths = allPaths(source)
paths.some(path => {
const sourceValue = R.view(R.lensPath(path), source)
const foundTransform = allTransforms.some(t => {
try {
const out = t(sourceValue)
if (R.equals(value, out)) {
sourceLens = path
transform = t
return true
}
} catch (e) {}
})
return foundTransform
})
// return sourceLens and transform

That's it. This abstracts access to every property in the source object and in the destination object and allows quickly finding matching ones. Then we can just give it two objects and enjoy programming-free computing.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const source = {
name: {
first: 'joe',
last: 'smith'
},
occupation: 'plumber'
}
const destination = {
firstName: 'Joe',
job: 'plumber'
}
const verySmartBot = require('change-by-example')
const t = verySmartBot(source, destination)
// now give the "t" a different object
t({
name: {first: 'mary'},
occupation: 'driver'
})
// {firstName: 'Mary', job: 'driver'}

All thanks to lenses.

Links