UNPKG

nanoscope

Version:

A Lens Library for Javascript

345 lines (255 loc) 12.7 kB
<a href="https://github.com/5outh/nanoscope"><img src="NanoscopeLogo.png" width="400px"></img></a> ## 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. `Lens`es allow you to modify data in interesting ways with minimal code, and `nanoscope` contains many useful `Lens`es 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 `Lens`es 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: ```js 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: ```js 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: ```js 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: ```js var firstTwo = new nanoscope.SliceLens(0, 2); ``` 2. By specifying a python-style slice as a string as a single argument: ```js var firstTwo = new nanoscope.SliceLens('0:2'); ``` By using the second syntax, you can use any of the python type variants using `:`. For example: ```js // 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: ```js 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. ```js 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: ```js 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: ```js new nanoscope.PathLens('a.b.c.e.f.g').view({}).over(function (elem) { return elem * 2; }); ``` ... from producing this: ```js { 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: ```js 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: ```js 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: ```js 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: ```js 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: ```js 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 `Lens`es Consider a `Lens` that views an array and focuses on its first element. The `get` function for this `Lens` might look like this: ```js var get = function (arr) { return arr[0]; }; ``` ...and `map` might be defined like so: ```js 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 `Lens`es 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: ```js var nanoscope = require('nanoscope'), headLens = new nanoscope.Lens(get, map); ``` All valid `Lens`es 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.