Last active
June 5, 2025 19:20
-
-
Save gatlin/264d49cf322aef31beccf680093bef38 to your computer and use it in GitHub Desktop.
Simple implementation of lenses in JavaScript
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * (!) 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); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment