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 | O.foo = "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 | const read = property => object => object[property] |
We could make a "writer" similarly, and we could even curry the function to accept property, value and object in three separate calls
1 | const write = property => value => object => object[property] = value |
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 | const fooBarBazSet = write('foo.bar.baz') |
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 | const fooBarBazLens = R.lensPath(['foo', 'bar', 'baz']) |
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 | const fooLens = R.lensProp('foo') |
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.
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.
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 | var 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
This has a huge huge shortcoming, actually 2 of them.
- We usually have the input object and the desired output object, but not the code. We have to write the code.
- 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 | var tomato = { |
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 | var 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 | const allPaths = (obj) => ... |
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 | // find lens inside the source object |
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 | const source = { |
All thanks to lenses.