Skip to the content.

Lenses in Javascript

Imagine you have an array of strings and each string contains ‘|’. You want to trim a prefix before ‘|’ and ‘|’ itself, and remove empty and duplicated strings. You might write code like this in Javascript.

const _ = require('lodash');

console.log('# 1')

const strings = ['a|1', 'a|2', 'b|1', 'c|'];

const convertedStrings1 = _.chain(strings)
    .map(s => s.substr(s.indexOf('|') + 1))
    .reject(_.isEmpty)
    .uniq()
    .value();
console.log(convertedStrings1);

This is pretty straightforward. Then, imagine you need to apply the same algorithm to an array of objects. You’ll use value property of each object as a value. To do this, you can simply extend the previous code to work on an array of objects.

const objects = [
    {
        value: 'a|1',
        description: 'a-1'
    },
    {
        value: 'a|2',
        description: 'a-2'
    },
    {
        value: 'b|1',
        description: 'b-1'
    },
    {
        value: 'c|',
        description: 'c-'
    },
];

const convertedObjects1 = _.chain(objects)
    .map(o => ({
        value: o.value.substr(o.value.indexOf('|') + 1),
        description: o.description
    }))
    .reject(o => _.isEmpty(o.value))
    .uniqBy('value')
    .value();
console.log(convertedObjects1);

This is easy, but you’d want to reuse this algorithm for strings and objects. Having two codes for the same algorithm doesn’t sound a good idea especially when the algorithm gets more complex, right?

The simple solution is to pass two functions to this algorithm. The first function gets a string from an element in an array. The second function updates an element with a specific string. Note that the second function returns a new string or object that has an updated value instead of mutating its parameter.

console.log('\n# 2');

function convert2(array, getValue, setValue) {
    return _.chain(array)
        .map(element => {
            const value = getValue(element);
            return setValue(element, value.substr(value.indexOf('|') + 1));
        })
        .reject(o => _.isEmpty(getValue(o)))
        .uniqBy(getValue)
        .value();
}

You can use this function to convert an array of strings and an array of objects.

const convertedStrings2 = convert2(strings, _.identity, (_, v) => v);
console.log(convertedStrings2);

const convertedObjects2 = convert2(objects,
    o => o.value,
    (o, value) => ({
        value,
        description: o.description
    }));
console.log(convertedObjects2);

This works pretty fine, but isn’t it nice if we can pass one function instead of two? After all, both of these functions do something on a specific property of the value (or the value itself).

When it comes to combining a getter and a setter, lenses seem a good solution.

First, let’s define Functor interface. Classes implementing this interface can encapsulate a value passed to their constructor, and apply a function to their internal value (map method).

console.log('\n# 3');

class Functor {
    constructor(v) {}
    map(f) {}
}

Then we’ll define two classes implementing Functor; Identity and Const. These classes look similar, but while map of Identity applies a function to its internal value, map of Const ignores it.

class Identity extends Functor {
    constructor(v) {
        super(v);
        this._v = v;
    }

    get identity() {
        return this._v;
    }

    map(f) {
        return new Identity(f(this._v));
    }
}

class Const extends Functor {
    constructor(v) {
        super(v);
        this._v = v;
    }

    get const() {
        return this._v;
    }

    map(f) {
        return new Const(this._v);
    }
}

Using these classes, you can write a function to convert an array like this.

function convert3(array, lens) {
    const getValue = element => lens(v => new Const(v))(element).const;
    const setValue = (element, s) => lens(_.constant(new Identity(s)))(element).identity;
    return convert2(array, getValue, setValue);
}

Once you write “lenses” for strings and objects, you can pass them to this function to get the result.

const stringLens = convertIntoFunctor => string => convertIntoFunctor(string);
const objectLens = convertIntoFunctor => object => convertIntoFunctor(object.value).map(value => ({
    value,
    description: object.description,
}));

const convertedStrings3 = convert3(strings, stringLens);
console.log(convertedStrings3);

const convertedObjects3 = convert3(objects, objectLens);
console.log(convertedObjects3);

As you can see when you look at objectLens, a parameter to convertIntoFunctor defines getValue and a function passed to map defines setValue. So now we encapsulated two functions into one function in a clean way and pick them using Identity or Const functor.

These functions (stringLens and objectLens) are called “lenses” because they let you focus on a specific part of a value. For instance, stringLens lets you focus on the string value itself, and objectLens lets you focus on value property of the object value.

Furthermore, you can compose these lenses using function composition. Each lens is defined as a function taking a function and returns another function, so you can compose them using usual function composition. For instance, you can easily focus on foo.bar.baz by composing these lenses.

With lens function that creates a lens focusing on a specified property, you can apply convert4 to objects with nested properties.

console.log("\n# 4")

const lens = name => convertIntoFunctor => object => convertIntoFunctor(object[name]).map(value => ({
    ...object,
    [name]: value,
}));

console.log(convert3([
    {
        foo: {
            bar: {
                baz: 'x|1'
            }
        },
        description: 'x-1'
    }
], _.flowRight(lens('foo'), lens('bar'), lens('baz'))));

In Javascript, using functors and lenses may be overdoing though.