Skip to content

Instantly share code, notes, and snippets.

@gatlin
Last active June 5, 2025 19:20
Show Gist options
  • Select an option

  • Save gatlin/264d49cf322aef31beccf680093bef38 to your computer and use it in GitHub Desktop.

Select an option

Save gatlin/264d49cf322aef31beccf680093bef38 to your computer and use it in GitHub Desktop.
Simple implementation of lenses in JavaScript
/*
* (!) 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