Index

A Lens Library for Javascript

Installation is easy:

$ npm install nanoscope

What is a Lens?

A Lens is a construct that allow you to peer into data structures and modify their contents. At base level, a Lens consists of a getter and a mapping function over a specific sub-part of your data. Lenses allow you to modify data in interesting ways with minimal code, and nanoscope contains many useful Lenses that you can plug into your existing code and use right out of the box, that provide things like:

  • Safe traversal of deeply nested data structures
  • Easy access and modification of single array elements
  • Access and modification of complete array slices, including modifications of size
  • A wrapper for safe access and modification of data through any Lens

The Lens Interface

Lenses support the following operations:

  • get, which gets the value at the focus of the Lens
  • set, which sets the value at the focus of the Lens
  • map, which maps a function over the focus of the Lens
  • view, which sets the view of the Lens to a new value
  • compose, which composes the Lens with another lens, allowing sequencing of actions.
  • add, which adds another Lens focus to the lens, allowing multiple focal points.

Assuming headLens is a Lens that focuses on the first element of an array, they can be used like this:

headLens.get([1, 2, 3]); // => 1
// or
headLens.view([1, 2, 3]).get(); // =>  1

headLens.set([1, 2, 3], 99); // =>  [99, 2, 3]
// or
headLens.view([1, 2, 3]).set(99); // =>  [99, 2, 3]

headLens.map([1, 2, 3], function (elem) { return elem * 10; }); // =>  [10, 2, 3]
// or
headLens.view([1, 2, 3]).map(function (elem) { return elem * 10; }); // =>  [10, 2, 3]

headLens.compose(headLens).view([['what'], 2, 3]).get(); // =>  'what'

// Assume lastLens focuses on the last element
headLens.compose(lastLens).view([1, 2, 3]).get(); // => [1, 3]

Of particular interest is compose, which allows us to compose a headLens with a headLens to focus on an array's first element of it's first element, and add, which allows us to focus on both the first and last elements of the array in parallel.

IndexedLens

IndexedLenses focus on a single element of an array, specified by its index. headLens as shown above can be built using an IndexedLens like so:

var headLens = new nanoscope.IndexedLens(0);

This means that we are focusing on the 0-th element of an array. IndexedLenses are safe by default, which means that they will not throw errors when you try to access elements out of range. For example, headLens.view([]).get() will not throw an error. To make an unsafe IndexedLens, just use the Unsafe constructor:

var unsafeHeadLens = new nanoscope.IndexedLens.Unsafe(0);

In an unsafe IndexedLens, the following operations will throw an error:

  • get(), if the index is greater than or equal to the length of the array, and
  • set(), if the index is strictly greater than the length of the array (you may tack on items to the end of an array)

SliceLens

SliceLenses focus on a subarray within an array. They can be constructed in two ways:

  1. By specifying start (inclusive) and end (exclusive) indices in the constructor, like so:
var firstTwo = new nanoscope.SliceLens(0, 2);
  1. By specifying a python-style slice as a string as a single argument:
var firstTwo = new nanoscope.SliceLens('0:2');

By using the second syntax, you can use any of the python type variants using :. For example:

// a `Lens` that focuses on everything but the first element
var tailLens = new nanoscope.SliceLens('1:');

// a `Lens` that focuses on everything but the last element
var initLens = new nanoscope.SliceLens(':-1');

Negative indices are accepted, which count backwards from the end of the list.

SliceLenses can be used not only to modify the elements in each slice, but it can also modify the length of the slice. For example:

initLens.view([1, 2, 3, 4]).map(
    function (arr) {
        return _.map(
            arr,
            function (elem) {
                return elem * 2;
            }
        )
    });
// => [1, 4, 6, 4]

// Assume `sum` sums the elements in a list
initLens.view([1, 2, 3, 4]).map(sum);
// => [6, 4]

PathLens

PathLenses are used to access nested data inside dynamic objects. They are constructed by passing a string representation of the path followed to get to the element to focus on, separated by .. They are safe by default; this is best illustrated by an example.

var testObject = {
    a : {
        b: {
            c : 100
        }
    }
};

new nanoscope.PathLens('a.b.c').view(testObject).get();
// => 100

new nanoscope.PathLens('a.b.c.d.e.f').view(testObject).get();
// => null

new nanoscope.PathLens('a.b.c').view(testObject).set('foo');
// => testObject.a.b.c == 'foo'

new nanoscope.PathLens('a.b.c.d.e.f').view(testObject).set('foo');
// => testObject.a.b.c.d.e.f == 'foo'

Note that in the last call we're overwriting testObject.a.b.c; this is by design, but something to be aware of. If you prefer that your PathLens throw errors when keys in the path don't exist, you can use the PathLens.Unsafe constructor instead; these are constructed in the same way:

new nanoscope.PathLens.Unsafe('a.b.c.d.e.f').view(testObject).get();
// => TypeError: Cannot read property 'e' of undefined

One other thing to note is that when using over in any PathLens, if:

  1. You are accessing a field that didn't originally exist, and
  2. Your function returns undefined or null,

your structure will be unmodified. This prevents things like:

new nanoscope.PathLens('a.b.c.e.f.g').view({}).over(function (elem) { return elem * 2; });

... from producing this:

{ a: { b: { c: { d: { e: { f: { g: undefined } } } } } } }

Instead, it will not modify the object and, in this case, simply return {}.

Optional

Optional Lenses wrap any Lens in a function that catches any errors that may happen along the way. They are constructed with the Optional constructor, which takes any Lens as an argument, along with an optional errorHandler function, This function will be called on any errors that may occur during the execution of any Lens operations, and if omitted, these errors will cause get to silently return null, and set/map to silently return the object passed in. errorHandler may also be a default value that you would prefer to return upon any errors.

For example, we can take an Unsafe IndexedLens and wrap it in Optional in order to handle incoming errors as they are thrown:

var Optional = nanoscope.Optional,
    IndexedLens = nanoscope.IndexedLens,
    lens;

lens = new Optional( new IndexedLens.Unsafe(10) );
lens.view([]).get(); // => null
lens.view([]).set(0); // => []

lens = new Optional( new IndexedLens.Unsafe(10), 'FAIL!' );
lens.view([]).get(); // => 'FAIL!'
lens.view([]).set(0); // => 'FAIL!'

lens = new Optional( new IndexedLens.Unsafe(10), console.log);
lens.view([]).get(); // => logs 'Error: Array index 10 out of range', returns undefined
lens.view([]).set(0); // => logs 'Error: Array index 10 out of range', returns undefined

One major thing to note is that Optional Lenses do not catch errors from calls to unimplemented functions in Getters and Setters. That is, calling setter.get and getter.set will still fail. This is by design, as these types of errors are logical in nature and should be caught by the programmer in all cases.

Compose

Compose is a wrapper (like Optional) that takes two Lenses and returns a new Lens that first focuses on the focus of the first Lens, and then on the second, in sequence. The compose() method constructs a Compose Lens under the hood, so the behavior is exactly the same. For a short example, consider an object with an array for one of the keys:

var obj = {
    a: {
        anArray: [99, 2, 3, 4]
    }
}

And say that we want a Lens that focuses on the second object in anArray. We can easily accomplish this with composite lenses:

var lensA = new nanoscope.PathLens('a.anArray'),
    lensB = new nanoscope.IndexedLens(1),
    composite = new nanoscope.Compose(lensA, lensB)
    // or composite = lensA.compose(lensB);

composite.view(obj).get(); // => 2
composite.view(obj).set(1); // => { a: { anArray: [99, 1, 3, 4] } }

composite first looks at the focus of lensA, then at the focus of lensB starting at the focus of lensA and uses this as its own focus.

MultiLens

MultiLenses allow you to focus on many different things at once and return them all at once. MultiLens is a sort of concurrent version of Compose. It takes either an Array of Lenses or an object with Lenses as values, and produces a Lens whose focus is all of the focuses in this argument. If the argument is an object, get will name each of the outputs in an object; if not, it will return an array of unnamed results. set and map will set every focus of the lens.

MultiLenses can also be constructed using the add method in any Lens, just like compose above.

Here is a simple example of a MultiLens in action:

var arrayLenses = [
            new nanoscope.IndexedLens(0),
            new nanoscope.IndexedLens(1)
    ],
    objectLenses = {
        head: new nanoscope.IndexedLens(0),
        last: new nanoscope.IndexedLens(-1)
    },
    // A MultiLens built from an Array
    arrayMultiLens = new nanoscope.MultiLens(arrayLenses),
    // or arrayMultiLens = new nanoscope.IndexedLens(0).add(new nanoscope.IndexedLens(1));
    // A MultiLens built from an Object
    objectMultiLens = new nanoscope.MultiLens(objectLenses);

arrayMultiLens.view([1, 2]).get(); // => [1, 2]
arrayMultiLens.view([1, 2]).set('g'); // => ['g', 'g']

objectMultiLens.view([1, 2, 3]).get(); // => { head: 1, last: 3 }
objectMultiLens.view([1, 2, 3]).set('g'); // => ['g', 2, 'g']

Getters and Setters

Getters are Lenses that only support get(), and Setters are Lenses that only support over and set. Getters and Setters are constructed with get functions and over functions only, respectively. They can also be constructed by using the fromLens static function in Getter and Setter, which simply replaces the old over/get operations in the original Lens. Constructing your own Lenses, Getters and Setters is described below, but here is an example of how fromLens works:

var Getter = nanoscope.Getter,
    Setter = nanoscope.Setter,
    IndexedLens = nanoscope.IndexedLens;

Getter.fromLens(new IndexedLens(0)).view([1]).get(); // => 1
Getter.fromLens(new IndexedLens(0)).view([1]).set(10); // => Error: map not permitted in a Getter

Setter.fromLens(new IndexedLens(0)).view([1]).set(10); // => [10]
Setter.fromLens(new IndexedLens(0)).view([1]).get(); // => Error: get not permitted in a Setter

These are useful when you want to restrict access to certain parts of your structures, but still use any Lenses to access data in one or more ways.

Making your own Lenses

Consider a Lens that views an array and focuses on its first element. The get function for this Lens might look like this:

var get = function (arr) {
    return arr[0];
};

...and map might be defined like so:

var map = function (arr, func) {
    var newArr = _.cloneDeep(arr);
    newArr[0] = func(newArr[0]);
    return newArr;
};

There are a couple of things to note here:

  1. We use _.cloneDeep from lodash (or underscore) to clone the object, because Lenses should provide immutable access to data.
  2. func is a function that operates on the focus of the Lens, which in this case is arr[0].
  3. We return the full, modified structure at the end.

We can construct a Lens from these bindings like so:

var nanoscope = require('nanoscope'),
    headLens = new nanoscope.Lens(get, map);

All valid Lenses must also satisfy the so-called "Lens Laws":

  1. set-get (you get what you put in): lens.get(lens.set(a, b)) = b
  2. get-set (putting what is there doesn't change anything): lens.set(a, lens.get(a)) = a
  3. set-set (setting twice is the same as setting once): lens.set(c, lens.set(b, a)) = lens.set(c, a)

These laws ensure that map, set and get behave in the manner you'd expect. If you can convince yourself that these laws are satisfied, you can rest easy knowing your Lens is well-behaved.