/* * (!) You can go to https://repl.it/languages/javascript, paste all this code * in there, and try it out for yourself! */ 'use strict'; /* * * What is a lens? * * A lens, basically, takes a structure and breaks out a part from the whole, * allowing you to do something with or to the part before reconstructing the * whole. Much like a tangible optical lens it lets you focus on some smaller * part of a larger object. * * That "something" can be changing the value or merely passing the value * elsewhere to be read. * * More technically, a lens is a function of three arguments: * * 1. Some structure (eg an Object, an Array, etc); * 2. An index specifying some piece of the structure; and * 3. A function which transforms that piece * * The lens then returns a new structure, with its transformed piece inside. * * Lenses have several very neat properties. * * First, a lens defined for part of some structure may be used as both a * setter and a getter. This means you only need to define a lens once to use * it for reading and writing. * * Because lenses are functions, they may be composed. * * First, let's define some things we know we'll need, to wit: * * - A way to replace part of a structure at a certain key with a new value; * - A way to compose functions. */ if(!Function.prototype.compose) { Function.prototype.compose = function(g) { let f = this; return function(x) { return f(g(x)); }; }; } let replace = function(key, value, thing) { thing[key] = value; return thing; }; /* * Now, on to lenses. A lens breaks a part from a whole, passes the part to a * function, and puts the result back into the whole. Intuitively, then, * whether or not a lens is used as a setter or a getter rests on that function * we pass in. * * This function we pass to the lens must return something we can put back in * the whole -- or at the very least, lead us to something which fits the bill. * * My strategy will be to write two functions which **both** take a value and * return an object with two properties: * * - a reference to the wrapped-up value; and * - a function to let us transform that value. * * Some of you may have already guessed it, but I'm basically defining two * functors: */ let Setter = (x) => Object.seal({ value: x, map: (f) => Setter(f(x)) }); let Getter = (x) => Object.seal({ value: x, map: (f) => Getter(x) }); let extract = (s) => s.value; let setTest = Setter(5); console.log(extract(setTest.map((x) => x*2))); // 10 let getTest = Getter(5); console.log(extract(getTest.map((x) => x*2))); // 5 /* * The key here is uniformity: both Setter and Getter wrap up a value and allow * me to at least pretend I'm transforming the value inside. Setter lets me do * it; Getter doesn't. Simple. * * The uniform way to transform the value inside is the `map()` method. * * Armed with these two functions, we can finally write the lens function: */ let lens = (key) => (fn) => (s) => fn(s[key]).map((v) => replace(key, v, s)); /* * Note that I'm writing this function in *curried* style. This is mostly for * stylistic reasons but it does make the code more readable and usable later * on. You'll see. * * So now we can create lenses! */ let myself = { name: 'gatlin', age: 27, dogs: [{ name: 'louie', breed: 'pug' }, { name: 'pugzy', breed: 'pug' }] }; let nameL = lens('name'); // <-- this is why we curried the function let ageL = lens('age'); let dogsL = lens('dogs'); let dogAt = (idx) => dogsL.compose(lens(idx)); let rename = (thing, newName) => set(nameL)(newName)(thing); /* * So how do we *use* these fancy new lenses? Lets first tackle reading, or * "getting", a value from a structure using a lens: */ let get = (l) => (s) => extract.compose(l(Getter))(s); /* * This is a function which takes a lens and a structure. The `l` variable is a * lens, which means that it is going to need a transformation function and * then a value. so `l(Getter)` returns a function that needs a structure given * to it (which would be `s`). However, ultimately we don't want a `Getter` * object, we want the value inside it. So we compose `extract` with all this * to retrieve the final value. * * As for setting, that's a special case of a more general operation: * transforming a part of a whole. Replacement is one kind of transformation. * For historical reasons let's define this more general kind of mutation as * `over`: */ let over = (l) => (f) => (s) => extract.compose(l(Setter.compose(f)))(s); /* * Let's unpack this. `over` requires a lens, a transformation function, and a * structure. We compose `Setter` and our transformation function, creating a * `Setter` containing the new value we want to put back in the whole. Then we * pass this resulting function to our lens `l`, creating a new function asking * for a structure. And again, as with `Getter`, we ultimately want the object * back, not the `Setter` object, so we compose `extract` with all this. * * Pause and re-read this a few times if you need to; I'll wait. * * So actually *setting* a value is a special case of `over` where our * transformation function takes the value we want to set, the original value, * and returns the value we want to set. This function is called `constant`: */ let constant = (x) => (y) => x; let set = (l) => (v) => (s) => over(l)(constant(v))(s); /* * Now let's try out our lenses! */ console.log(myself); rename(myself, 'Gatlin'); console.log(get(nameL)(myself)); // Lenses compose! set( dogAt(0).compose(nameL) )('Louie')(myself); // But we can also retrieve a part and use existing lens-based functions on them! let pugzy = get(dogAt(1))(myself); rename(pugzy,'Pugzy'); console.log(myself);