nanoscope
Version:
A Lens Library for Javascript
316 lines (273 loc) • 8.22 kB
JavaScript
"use strict";
/**
* A `Lens` is a construct that allows you to 'peer into' some structure and operate on sub-parts of it. A `Lens` supports
* three basic operations:
*
* 1. `get`, which takes an object and gets a piece of it,
* 2. `map`, which takes an object and maps a function over it, and
* 3. `set`, which takes an object and sets it to some value.
*
* To construct a `Lens`, you must provide both a `get` function and an `map` function. `set` is a special case of `map`, so you
* don't need to explicitly define it.
*
* As a simple example, the following constructs a `Lens` that focuses on the first element of an array:
*
* ```javascript
* var headLens = new Lens (
* function (arr) { return arr[0]; },
* function (arr, func) {
* var newArr = _.deepClone(arr); // Lenses should operate on immutable data, don't modify original array
* newArr[0] = func(newArr[0]); // Apply a user-specified function to the head of the array and set the first element
* return newArr; // Return the modified array
* }
* );
* ```
*
* Any user-constructed lenses are expected to obey the Lens laws, as follows:
*
* 1. set-get (you get what you put in): `lens.get(a, 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 the getting and setting behavior make sense in the usual way.
*
* @param {function} get Get the value you want from the structure
* @param {function} map Map a function map the value and return the modified structure
* @param {object} flags Additional properties to add to `Lens` if specified
* @returns {Lens}
* @constructor
*/
var _ = require('lodash'),
Lens;
Lens = function (get, map, flags) {
var self = this;
self._flags = flags || {};
self._get = get;
self._over = map;
self.then = self;
// Set view from constructor
if (self._flags._view) {
self._view = _.clone(self._flags._view);
}
return self;
};
/**
* Get the value this `Lens` focuses on from an object
*
* @param {*} obj The object to run the `Lens` on
* @returns {*}
*/
Lens.prototype.get = function (obj) {
return this._get(obj || this._view);
};
/**
* Run a function over the view of the `Lens` and return the modified structure
*
* @param {*} obj The object to run the `Lens` on
* @param {function} func The function to call on the view of the Lens
* @returns {Lens}
*/
Lens.prototype.map = function (obj, func) {
// If a view exists and a second argument isn't provided, use the view.
if (this._view != null && !func) {
return this._over(this._view, obj);
}
return this._over(obj, func);
};
/**
* Map a function over the focus of this lens and return a new lens focusing
* on the modified object.
*
* @param {[type]} obj [description]
* @param {[type]} func [description]
* @return {[type]} [description]
*/
Lens.prototype.mapping = function (obj, func) {
this.view(this.map(obj, func));
return this;
};
/**
* Set the view of the `Lens` to something new and return the modified structure
*
* @param {*} obj The object to run the Lens on
* @param {*} val The value to set
* @returns {Lens}
*/
Lens.prototype.set = function (obj, val) {
// If a view exists, and a second argument isn't provided, set the view.
if (this._view != null && _.isUndefined(val)) {
return this.map(this._view, _.constant(obj));
}
return this.map(obj, _.constant(val));
};
/**
* Set the value being focused on and return a new lens focusing on the modified object.
*
* @param {[type]} obj [description]
* @param {[type]} val [description]
* @return {[type]} [description]
*/
Lens.prototype.setting = function (obj, val) {
this.view(this.set(obj, val));
return this;
};
/**
* Force the `Lens` to `view` a new object
*
* @param {*} view The object to view a Lens on
* @return {Lens} this
*/
Lens.prototype.view = function (view) {
this._view = view;
this._flags._view = view;
return this;
};
// Alias for view
Lens.prototype.viewing = Lens.prototype.view;
/**
* Reset the view of the `Lens`.
*
* @return {Lens} this
*/
Lens.prototype.blur = function () {
this._view = null;
return this;
};
/**
* Get any extra set options in a Lens
*
* @returns {*}
*/
Lens.prototype.getFlags = function () {
return this._flags;
};
/**
* Get a specific flag from a Lens
*
* @param flag
* @returns {*}
*/
Lens.prototype.getFlag = function (flag) {
return this._flags[flag];
};
/**
* Add a flag to the Lens
*
* @param {*} flag
*/
Lens.prototype.addFlag = function (flag) {
this._flags = _.extend(this._flags, flag);
};
/**
* Compose this lens with another `Lens`
*
* @param {Lens} otherLens The `Lens` to compose this one with
* @returns {Compose}
*/
Lens.prototype.compose = function (otherLens) {
var Compose = require('./../combinator/Compose');
return new Compose(this, otherLens, _.extend(this.getFlags() || {}, otherLens.getFlags()));
};
/**
* Compose this lens with many other Lenses, specified by an array in which to order them.
*
* @param otherLenses
* @returns {Lens}
*/
Lens.prototype.composeMany = function (otherLenses) {
var args = arguments,
lens = this;
// Support variable length args
if (args.length > 1) {
otherLenses = args;
}
_.forEach(otherLenses, function (otherLens) {
lens = lens.compose(otherLens);
});
return lens;
};
/**
* Add a new focus to this `Lens` by providing another `Lens` with which to focus with.
*
* @param otherLens The `Lens` to add to this `Lens`
* @returns {MultiLens}
*/
Lens.prototype.add = function (otherLens) {
var MultiLens = require('./../combinator/MultiLens');
return new MultiLens([this, otherLens], _.extend(this.getFlags(), otherLens.getFlags()));
};
/**
* Add many new focuses to this `Lens` by providing an array of other lenses to focus with.
*
* @param otherLenses
* @returns {Lens}
*/
Lens.prototype.addMany = function (otherLenses) {
var args = arguments,
lens = this;
// Support variable length args
if (args.length > 1) {
otherLenses = args;
}
_.forEach(otherLenses, function (otherLens) {
lens = lens.add(otherLens);
});
return lens;
};
/**
* Create a disjunction between this lens and another.
*
* @param otherLens
* @returns {DisjunctiveLens}
*/
Lens.prototype.or = function (otherLens) {
var DisjunctiveLens = require('../combinator/DisjunctiveLens');
return new DisjunctiveLens(this, otherLens, _.extend(this.getFlags() || {}, otherLens.getFlags()));
};
/**
* Create a conjunction between this lens and another.
*
* @param otherLens
* @returns {ConjunctiveLens}
*/
Lens.prototype.and = function (otherLens) {
var ConjunctiveLens = require('../combinator/ConjunctiveLens');
return new ConjunctiveLens(this, otherLens, _.extend(this.getFlags() || {}, otherLens.getFlags()));
};
/**
* Focus on every element of an array at once
*
* @param eachFn
* @returns {Compose}
*/
Lens.prototype.each = function (eachFn) {
var MultiLens = require('../combinator/MultiLens'),
IndexedLens = require('../array/IndexedLens'),
arr = this.get(),
lenses = [];
if (_.isArray(arr)) {
lenses = _.map(_.range(arr.length), function (idx) {
return eachFn(new IndexedLens(idx));
});
}
return this.compose(new MultiLens(lenses, this.getFlags()));
};
/**
* Focus on every element of an object at once
*
* @param ownFn
* @returns {Compose}
*/
Lens.prototype.own = function (ownFn) {
var MultiLens = require('../combinator/MultiLens'),
PathLens = require('../object/PathLens'),
obj = this.get(),
lenses = [];
if (_.isObject(obj)) {
_.forEach(_.keys(obj), function (key) {
lenses.push(ownFn(new PathLens(key).view(obj[key])));
});
}
return this.compose(new MultiLens(lenses, this.getFlags()));
};
module.exports = Lens;