UNPKG

optika

Version:

Optics: modular data access for JavaScript

539 lines (433 loc) 12.5 kB
/** # Optika <img src="https://raw.githubusercontent.com/phadej/optika/master/optika-300.png" align="right" height="100" /> > Optics: modular data access [![Build Status](https://secure.travis-ci.org/phadej/optika.svg?branch=master)](http://travis-ci.org/phadej/optika) [![NPM version](https://badge.fury.io/js/optika.svg)](http://badge.fury.io/js/optika) [![Dependency Status](https://david-dm.org/phadej/optika.svg)](https://david-dm.org/phadej/optika) [![devDependency Status](https://david-dm.org/phadej/optika/dev-status.svg)](https://david-dm.org/phadej/optika#info=devDependencies) ## Getting Started Install the module with: `npm install optika` ## Synopsis ```javascript var o = require("optika"); var value = { foo: [ { bar: 2, baz: 3 }, { bar: 4, baz: 5 }, ] }; // [2, 4] o.key("foo").traversed().key("bar").arrayOf(value); // { foo: [ { bar: 9, baz: 3 }, { bar: 11, baz: 5 } ] } o.key("foo").traversed().key("bar").over(value, function (x) { return x + 7; }); ``` ## Motivation [Immutable.js](https://facebook.github.io/immutable-js/) is great! But working with immutable containers & nested records is un-easy: ```javascript return data.update("months", months => months.map(month => month.update("days", days => days.map(day => injectAuxData(day, auxiliaryData)) ) ) ); ``` The `updateIn` isn't powerful enough to make the drilling less boilerplaty (and less error-prone). If you are a Haskell programmer, you might know that *lenses* are a solution to this problem: ```haskell data_ & months . traversed . days . traversed %~ \day -> injectAuxData day auxData -- or without operators: over (months . traversed . days . traversed) (\day -> injectAuxData day auxData) data_ ``` And with this library you can write similar code in JavaScript: ```javascript o.imkey("months").traversed().imkey("days").traversed().over(data, day => injectAuxData(day, auxData) ); ``` */ "use strict"; /** ## Documentation There is small amount of run-time validation. For example, if you try to *set* over `Getter`, the exception will be thrown: ```javascript o.key("foo").to(function (x) { return x[0]; }).set(value, 42); // throws: Trying to run Traversal operation via Getter ``` */ // var assert = require("assert"); var monoid = require("./monoid.js"); var profunctor = require("./profunctor.js"); var u = require("./utils.js"); // var pl = require("partial.lenses"); function Optic(classes, parts) { u.assert(u.isString(classes), "new Optic: classes should be string"); u.assert(u.isArray(parts), "new Optic: parts should be an array"); this.classes = classes; this.parts = parts; } /** ### Operations */ /** - `get(this: Getter<S,A>, value: S): A` */ Optic.prototype.get = function (value) { var f = this.run(profunctor.dictForgetNone, u.identity); var x = f(value); return x; }; /** - `set(this: Traversal<S,T,A,B>, value: S, b: B): T` */ Optic.prototype.set = function (value, b) { return this.over(value, function () { return b; }); }; /** - `over(this: Traversal<S,T,A,B>, value: S, f: A => B): T` */ Optic.prototype.over = function (value, g) { var f = this.run(profunctor.dictFunc, g); var x = f(value); return x; }; /** - `review(this: Prism<S,T,A,B>, value: B): T` - `review(this: Iso<S,T,A,B>, value: B): T` Construct value thru `Prism` or `Iso`. */ Optic.prototype.review = function (value) { return this.run(profunctor.dictTagged, value); }; /** - `affineView(this: Affine<S,T,A,B>, def: A): A For operation working with `Fold`, see `firstOf`. */ Optic.prototype.affineView = function (value, def) { var f = this.run(profunctor.dictForgetMaybe, u.identity); var x = f(value); return x === profunctor.dictForgetMaybe.def ? def : x; }; /** - `reduceOf(this: Fold<S,T,A,B>, value: S, init: I, combine: (I, A) => I): I` Fold starting with initial value `init`, and combining results with `combine`, in the `this` Fold's order. */ // pre-lookup tags var wanderTag = profunctor.dictNeglect.wanderTag; var firstTag = profunctor.dictNeglect.firstTag; // var rightTag = profunctor.dictNeglect.rightTag; Optic.prototype.reduceOf = function (v, init, snoc, append) { var commands; if (!this.commandsNeglect) { commands = this.run(profunctor.dictNeglect, []).reverse(); this.commandsNeglect = commands; } else { commands = this.commandsNeglect; } // accumulator var acc = init; // command "pointer" var cmdIdx = 0; // We create function // - it creates closure over acc, so it cannot be top-level // - we use it to recurse while wandering // - should we manage stack explicitly, and avoid functions? function process(value) { var j; if (cmdIdx < commands.length) { var cmd = commands[cmdIdx]; if (cmd === wanderTag) { if (append && cmdIdx + 1 === commands.length) { acc = append(acc, value); } else if (u.isArray(value)) { cmdIdx += 1; for (j = 0; j < value.length; j++) { process(value[j]); } cmdIdx -= 1; } else { cmdIdx += 1; value.forEach(function (x) { process(x); }); cmdIdx -= 1; } } else if (u.isString(cmd)) { cmdIdx += 1; process(value[cmd]); cmdIdx -= 1; } else if (u.isFunction(cmd)) { cmdIdx += 1; process(cmd(value)); cmdIdx -= 1; } else if (cmd === firstTag) { cmdIdx += 1; process(value[0]); cmdIdx -= 1; } else /* if (cmd === rightTag) */ { cmdIdx += 1; if (value[0]) { // right: loop process(value[1]); } else { // left: do nothing } cmdIdx -= 1; } } else { acc = snoc(acc, value); } } // interpret the program process(v); return acc; }; /** - `arrayOf(this: Fold<S,T,A,B>, value: S): Array<A>` */ Optic.prototype.arrayOf = function (value) { return this.reduceOf(value, monoid.array.init(), monoid.array.snoc, monoid.array.append); }; Optic.prototype.forgetArrayOf = function (value) { var f = this.run(profunctor.newDictForget(monoid.array), u.arraySingleton); var x = f(value); return x; }; /** - `sumOf(this: Fold<S,T,number,B>, value: S): number` */ Optic.prototype.sumOf = function (value) { return this.reduceOf(value, monoid.sum.init(), monoid.sum.snoc, monoid.sum.append); }; Optic.prototype.forgetSumOf = function (value) { var f = this.run(profunctor.newDictForget(monoid.sum), u.identity); var x = f(value); return x; }; /** ### Constructors */ function lensSetter(tuple) { return tuple[1](tuple[0]); } /** - `lens(getter: S => A, setter: (S, B) => T): Lens<S,T,A,B>` */ function lens(getter, setter) { u.assert(u.isFunction(getter), "lens: getter should be a function"); u.assert(u.isFunction(setter), "lens: setter should be a function"); function getter2(s) { return [getter(s), function (b) { return setter(s, b); }]; } return new Optic("strong", [function (dict, x) { var x0 = dict.first(x); var x1 = dict.dimap(getter2, lensSetter, x0); return x1; }]); } var traversedCached = new Optic("wander", [function (dict, x) { return dict.wander(x); }]); /** - `traversed(): Traversal<F<A>,F<B>,A,B>` Creates traversal for everything with `.map` and `.forEach`. */ function traversed() { return traversedCached; } /** - `to(f: S => A): Getter<S,A>` */ function to(f) { return new Optic("bicontra", [function (dict, x) { return dict.cimap(f, f, x); }]); } /** ### Convenient optics */ /** - `key(K: keyof (S & T)): Lens<S,T,S[K],T[K]>` Works for *plain-old-javascriot-objects*, i.e. POJOs :) */ function key(k) { u.assert(u.isString(k), "key: k should be a string"); return new Optic("strong", [function (dict, x0) { var x1 = dict.key(k, x0); return x1; }]); } /** - `idx(i: number)): Lens<Array<A>,Array<A>,A,A>` */ function idx(i) { u.assert(u.isNumber(i), "idx: i should be a number"); function getter(s) { return [s[i], s]; } function setter(p) { var t = p[1].slice(); t[i] = p[0]; return t; } return new Optic("strong", [function (dict, x0) { var x1 = dict.first(x0); var x2 = dict.dimap(getter, setter, x1); return x2; }]); } /** - `imkey(K: keyof (S & T)): Lens<Record<S>,Record<T>,S[K],T[K]>` Works with everyting supporting `.get` and `.set`, e.g. [Immutable](http://facebook.github.io/immutable-js/). */ function imkey(k) { u.assert(u.isString(k), "imkey: k should be a string"); function getter(s) { return [s.get(k), s]; } function setter(p) { return p[1].set(k, p[0]); } return new Optic("strong", [function (dict, x0) { var x1 = dict.first(x0); var x2 = dict.dimap(getter, setter, x1); return x2; }]); } /** - `imidx(i: number)): Lens<List<A>,List<A>,A,A>` Works with everyting supporting `.get` and `.set`, e.g. [Immutable](http://facebook.github.io/immutable-js/). *Note:* doesn't perform bounds check. */ function imidx(i) { u.assert(u.isNumber(i), "imidx: i should be a number"); function getter(s) { return [s.get(i), s]; } function setter(p) { return p[1].set(i, p[0]); } return new Optic("strong", [function (dict, x0) { var x1 = dict.first(x0); var x2 = dict.dimap(getter, setter, x1); return x2; }]); } // prism :: forall s t a b. (b -> t) -> (s -> Either t a) -> Prism s t a b // prism to fro pab = dimap fro (either id id) (right (rmap to pab)) /** - `filtered(pred: A => boolean): Prism<A,A,A',A'>` *Note*: The predicate is checked when the value is injected (via `Traversal`) or constructed via `Prism`. */ function filtered(pred) { u.assert(u.isFunction(pred), "filtered: pred should be a function"); // s -> Either t a function filteredGetter(x) { return [pred(x), x]; } return new Optic("choice", [function (dict, x) { var x0 = dict.right(x); var x1 = dict.dimap(filteredGetter, u.snd, x0); return x1; }]); } /** - `safeFiltered(pred: A => boolean): Prism<A,A,A',A'>` Like `filtered` but predicate is checked on construction. */ function safeFiltered(pred) { u.assert(u.isFunction(pred), "safeFiltered: pred should be a function"); // s -> Either t a function filteredGetter(x) { return [pred(x), x]; } function check(x) { // TODO: throw own exception. u.assert(pred(x), "safeFiltered: construction of invalid value"); return x; } return new Optic("choice", [function (dict, x) { var x0 = dict.dimap(u.identity, check, x); var x1 = dict.right(x0); var x2 = dict.dimap(filteredGetter, u.snd, x1); return x2; }]); } /** ### Internals Functions which you probably never need to use directly. */ /** `.o(this: Optic<S,T,A,B>, other: Optic<A,B,U,V>): Optic<S,T,U,V>` Compose two optics. */ Optic.prototype.o = function o(other) { var self = this; var classes = profunctor.normaliseClasses(self.classes, other.classes); return new Optic(classes, other.parts.concat(this.parts)); }; /** `.run(this: Optic<S,T,A,B>, p: Profunctor<A,B>): Profunctor<S,T>` */ Optic.prototype.run = function (dict, x) { var thisClasses = this.classes; var combined = profunctor.normaliseClasses(thisClasses, dict.classes); u.assert(combined === dict.classes, function () { return [ "Trying to run", profunctor.classesToOpticName(dict.classes), "operation via", profunctor.classesToOpticName(thisClasses), ].join(" "); }); var parts = this.parts; for (var i = 0; i < parts.length; i++) { x = parts[i](dict, x); } return x; }; // Exports var constructors = [ filtered, idx, imidx, imkey, key, lens, safeFiltered, to, traversed, ]; var optics = {}; constructors.forEach(function (ctr) { // export optics[ctr.name] = ctr; // Optics.prototype postcomposed Optic.prototype[ctr.name] = function () { var other = ctr.apply(undefined, arguments); return this.o(other); }; }); // Additional exports module.exports = optics; // plain ../FAQ.md /// plain ../CONTRIBUTING.md /// plain ../CHANGELOG.md /// plain ../implementation-details.md /// plain ../related-work.md /// plain ../LICENSE