Lenses Under the Hood
We explore what makes lenses tick, and experiment with functors along the way. (12-15 min. read)
We'll cover the following...
Check out Ramda’s lens source code. As of this course, it looks like this
var lens = _curry2(function lens(getter, setter) {return function(toFunctorFn) {return function(target) {return map(function(focus) {return setter(focus, target);},toFunctorFn(getter(target)));};};});
There’s a lot to unpack here but I think we can do it. Starting with the first line…
Currying
Look at the top of the source code file.
import _curry2 from './internal/_curry2';
Ok, not too scary–they imported an internal _curry2 function. Why?
var lens = _curry2(function lens(getter, setter) {
It’s currying lens. Ramda curries everything so _curry2 seems to be specialized for functions with two arguments.
Why a specialized curry()?
Ramda’s curry is dynamic, and handles all types of functions with differing argument counts.
If you know how many arguments you need, however, using a specialized curry can optimize your function’s run-time. You can ignore handling any other argument case, because you know your function expects 2, 3, 4, etc arguments.
Next line!
var lens = _curry2(function lens(getter, setter) {return function(toFunctorFn) {
So lens returns a function after receiving its getter and setter. Let’s test that out.
import { assoc, lens, prop } from 'ramda';const name = lens(prop('name'), assoc('name'));console.log(name.toString());
Yep! This returned a function that takes one parameter, toFunctorFn. “To Functor Function”
Sounds like a function that turns something into a functor. We know from the previous section that functors are containers that can hold any value.
Using Functors
What’s the next step after giving lens a getter/setter? Pass it to view, set, or over.
import { assoc, lens, prop, view } from 'ramda';const name = lens(prop('name'), assoc('name'));const result = view(name, { name: 'Bobo' });console.log({ result });
That’s pretty cool. With the getter/setter a lens looks like this
function (toFunctorFn) {return function (target) {return map(function (focus) {return setter(focus, target);}, toFunctorFn(getter(target)));};}
After giving it to view, though…
view(name, { name: 'Bobo' });
…everything’s magically resolved and we get 'Bobo'.
View Magic
That means view satisfied all of lens’ requirements in a single shot, so it’s our next point of investigation. Take a look at its source code. There’s a variable, Const.
var Const = function(x) {return {value: x,'fantasy-land/map': function() { return this; }};};console.log(Const('Hello World'));
It creates a functor with two properties
valuefantasy-land/mapmethod
We’ve already seen this. It wants to override map's functionality.
Now look at view's implementation.
var view = _curry2(function view(lens, x) {return lens(Const)(x).value;});
Const is the toFunctorFn here. It will tell lens how to turn something into a functor.
Let’s extract that step into our own playground to experiment a little. We just need to define our own Const and give it to the name lens we created earlier.
import { assoc, lens, prop, view } from 'ramda';// Define Constvar Const = function(x) {return {value: x,'fantasy-land/map': function() { return this; }};};const name = lens(prop('name'), assoc('name'));// And use it hereconst withFunctorFn = name(Const);console.log(withFunctorFn.toString());
Yet Another Function
It returned another function, when will the madness end?!
function (target) {return map(function (focus) {return setter(focus, target);}, toFunctorFn(getter(target)));}
This one needs a target. Let’s see how view satisfies this requirement.
var view = _curry2(function view(lens, x) {return lens(Const)(x).value;});
view's second argument, x
function view(lens, x)
gets forwarded to lens
lens(Const)(x)
Back to the lab!
import { assoc, lens, prop, view } from 'ramda';// Define Constvar Const = function(x) {return {value: x,'fantasy-land/map': function() { return this; }};};const name = lens(prop('name'), assoc('name'));// Use it hereconst withFunctorFn = name(Const);// Then pass a targetconst withTarget = withFunctorFn({ name: 'Bobo' });console.log(withTarget);
Whoa whoa, look at that!
{
value: 'Bobo',
'fantasy-land/map': [Function: fantasyLandMap]
}
That’s our desired value, Bobo, after passing through Const. It’s now a functor!
console.log(Const('Bobo'));
So we somehow get back a functor(Bobo) after this code runs
return function(target) {return map(function(focus) {return setter(focus, target);},toFunctorFn(getter(target)));};
After getting its target, lens begins mapping.
Why map()?
But mapping is to change a functor’s value. view's only supposed to get a value, not modify it!
map does change values, but not if you override it…
Let’s dissect the following code snippet.
return map(function(focus) {return setter(focus, target);},toFunctorFn(getter(target)));
What are the pieces?
getter = prop('name');target = { name: 'Bobo' };toFunctorFn = function(x) {return {value: x,'fantasy-land/map': function() { return this; }};};
Look closer at that functor’s fantasy-land/map method.
`fantasy-land/map`: function() { return this; }
Overridden to Do Nothing
It does nothing but return itself.
This makes sense because view's job is to return the data untouched. By returning this it effectively ignores map.
To contrast, look at over's toFunctorFn in the source code.
var Identity = function(x) {return {value: x, map: function(f) { return Identity(f(x)); }};};
It’s an Identity functor, similar to the previous lesson!
Instead of doing nothing like view, map's being told to transform the lens value according to the supplied function.
import { assoc, lens, over, prop, toUpper } from 'ramda';const name = lens(prop('name'), assoc('name'));const result = over(name, toUpper, { name: 'Bobo' });console.log({ result });
So whether you’re reading with view or writing with set/over, your toFunctorFn is the key to making it all work with map.
Summary
-
Lenses use functors, therefore implement everything, even reading a value, with
map. -
This is possible because an object can define its own
fantasy-land/mapmethod to take full control of the procedure. -
In
view's case,fantasy-land/mapjust returns itself (the requested data). -
In
over's case,fantasy-land/maptransforms the data with a given function.
We’re exploring how lenses compose next. That’s right, we’re going to start combining them to find shocking results!