Skip to content

Instantly share code, notes, and snippets.

@rikkertkoppes
Last active May 18, 2017 08:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rikkertkoppes/37509632d24d3695650046f53f4512b4 to your computer and use it in GitHub Desktop.
Save rikkertkoppes/37509632d24d3695650046f53f4512b4 to your computer and use it in GitHub Desktop.

Lenses

My attempt to understand and create functional lenses in javascript. There are some laws and de facto standards

  • get after set should return the modified part
  • set after get should return the unmodified whole
  • set after set should overwrite the part
  • create should take a getter and a setter
  • lenses should be left to right composable
  • there should be a view, set and over functions to work with lenses
  • there should be index, prop and path functions to create lenses

naive implementation

Just create a getter and setter object

export const Lens = {
    create(getter, setter) {
        return {getter, setter}
    },
    view(lens, w) {
        return lens.getter(w);
    },
    set(lens, v, w) {
        return lens.setter(w)(v);
    },
    over(lens, m, w) {
        return lens.setter(w)(m(lens.getter(w)));
    }
}

The way this works is you create a lens by specifying a getter and a setter:

let whole = {foo: 2}
let fooLens = Lens.create(w => w.foo, w => v => ({...w, foo: v}));

Which you can then use to get and set a value:

Lens.view(fooLens, whole); // => 2
Lens.set(fooLens, 4, whole); // => {foo: 4}
Lens.over(fooLens, x => 2*x, whole); // => {foo: 4}

This works but it is not composable. To make it composable, create should at least return a function.

return a function

export const Lens = {
    create(getter, setter) {
        return () => ({getter, setter})
    },
    view(lens, w) {
        return lens().getter(w);
    },
    set(lens, v, w) {
        return lens().setter(w)(v);
    },
    over(lens, m, w) {
        return lens().setter(w)(m(lens().getter(w)));
    }
}

This is composable alright, but really does not do what we want. Let's look at what argument the lens gets passed in when it is composed.

Suppose we have an object

let whole = {foo: {bar: 2}}

a lens for the foo property and a lens for the bar property

let fooLens = Lens.create(w => w.foo, w => v => ({...w, foo: v}));
let barLens = Lens.create(w => w.bar, w => v => ({...w, bar: v}));

and we want it to compose left to right

let fooBarLens = compose(fooLens, barLens);

so the fooLens gets as its argument the result of the barLens function, which is a getter-setter pair. Lets call this inner

export const Lens = {
    create(getter, setter) {
        return (inner) => {
            return {
                getter,
                setter
            }
        }
    },
    //...
}

composition for view

Let's look at getter composition. The fooLens function gets the result of the barLens function, which contains a getter for bar. This getter for bar expects the value of foo as the whole, which we can get with the getter for foo:

export const Lens = {
    create(getter, setter) {
        return (inner) => {
            return {
                getter: (w) => inner.getter(getter(w)),
                setter
            }
        }
    },
    //...
}

Pause for a moment and make sure you understand. It took me a while.

Fixing view, set and over

We have a broken implementation now. Let's fix that

const id = x => x;
export const Lens = {
    create(getter, setter) {
        return (inner) => {
            return {
                getter: (w) => inner.getter(getter(w)),
                setter
            }
        }
    },
    view(lens, w) {
        return lens({getter: id}).getter(w);
    },
    set(lens, v, w) {
        return lens({}).setter(w)(v);
    },
    over(lens, m, w) {
        return lens({}).setter(w)(m(lens({getter: id}).getter(w)));
    }
}

As the lens now expects an argument, we pass in the identity function as a getter for view and over. This makes sure that the composition in create still works. This just reduces to (w) => getter(w), which is what we had before.

Now get the setters composable

composition for set

Again, we have fooLens and barLens

let whole = {foo: {bar: 2}}
let fooLens = Lens.create(w => w.foo, w => v => ({...w, foo: v}));
let barLens = Lens.create(w => w.bar, w => v => ({...w, bar: v}));

For setting values in a composed way, barLens should operate on the result of fooLens. Remember that the barLens getter-setter pair is passed in as the inner argument in the fooLens function. The result of this setter is the value to be used for the fooLens setter.

So the setter is currently just

setter: (w) => (v) => setter(w)(v);

now, let the inner setter operate on the result of the outer getter

setter: (w) => (v) => inner.setter(getter(w))(v)

does this make sense? The outer getter returns the inner whole foo, which is {bar: 2}. Then the inner setter operates on it.

However, this results in the modified inner value. We are missing one step. Setting this modified inner value in the outer whole

setter: (w) => (v) => setter(w)(inner.setter(getter(w))(v))

which results in the following implementation:

export const Lens = {
    create(getter, setter) {
        return (inner) => {
            return {
                getter: (w) => inner.getter(getter(w)),
                setter: (w) => (v) => setter(w)(inner.setter(getter(w))(v))
            }
        }
    },
    //...
}

Fixing view, set and over

We again have a broken implementation now. Let's fix that. We need to pass something in as the inner setter. What should that be? Well, the inner setter should just return v, so that

setter: (w) => (v) => setter(w)(inner.setter(getter(w))(v))

becomes equivalent to

setter: (w) => (v) => setter(w)(v)

which is what we had before. To do this, the inner setter needs to be

setter: (w) => (v) => v

or just

setter: (w) => id

so

const id = x => x;
export const Lens = {
    create(getter, setter) {
        return (inner) => {
            return {
                getter: (w) => inner.getter(getter(w)),
                setter: (w) => (v) => setter(w)(inner.setter(getter(w))(v))
            }
        }
    },
    view(lens, w) {
        return lens({getter: id}).getter(w);
    },
    set(lens, v, w) {
        return lens({setter: _ => id}).setter(w)(v);
    },
    over(lens, m, w) {
        return lens({setter: _ => id}).setter(w)(m(lens({getter: id}).getter(w)));
    }
}

Let's refactor a bit

const id = x => x;
const idAccessor = {
    getter: id,
    setter: _ => id
};
export const Lens = {
    create(getter, setter) {
        return (inner) => {
            return {
                getter: (w) => inner.getter(getter(w)),
                setter: (w) => (v) => setter(w)(inner.setter(getter(w))(v))
            }
        }
    },
    view(lens, w) {
        return lens(idAccessor).getter(w);
    },
    set(lens, v, w) {
        return lens(idAccessor).setter(w)(v);
    },
    over(lens, m, w) {
        return lens(idAccessor).setter(w)(m(lens(idAccessor).getter(w)));
    }
}

Implementing index, prop and path

index

Index should get an index from an array. When setting, it should return a new array.

index(n) {
    return Lens.create(w => w[n], w => v => [...w.slice(0,n), v, ...w.slice(n+1)]);
}

prop

Props should return a value at a key from an object. When setting, it should return a new object.

prop(k) {
    return Lens.create(w => w[k], w => v => ({...w, [k]: v}));
}

path

Path is just repeated application of prop. For this we need a compose function. A simple implementation is this:

function compose(...fns) {
    if (fns.length === 0) {
        return x => x;
    }
    return fns.reduce((f, g) => (...args) => f(g(...args)));
}

As we already made sure lens composition works, the implementation of path is easy

path(p) {
    return compose(...p.split('.').map(Lens.prop));
}

Final implementation.

So everything together is this

function compose(...fns) {
    if (fns.length === 0) {
        return x => x;
    }
    return fns.reduce((f, g) => (...args) => f(g(...args)));
}

const id = x => x;
const idAccessor = {
    getter: id,
    setter: _ => id
};
export const Lens = {
    create(getter, setter) {
        return (inner) => {
            return {
                getter: (w) => inner.getter(getter(w)),
                setter: (w) => (v) => setter(w)(inner.setter(getter(w))(v))
            }
        }
    },
    view(lens, w) {
        return lens(idAccessor).getter(w);
    },
    set(lens, v, w) {
        return lens(idAccessor).setter(w)(v);
    },
    over(lens, m, w) {
        return lens(idAccessor).setter(w)(m(lens(idAccessor).getter(w)));
    },
    index(n) {
        return Lens.create(w => w[n], w => v => [...w.slice(0,n), v, ...w.slice(n+1)]);
    },
    prop(k) {
        return Lens.create(w => w[k], w => v => ({...w, [k]: v}));
    },
    path(p) {
        return compose(...p.split('.').map(Lens.prop));
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment