optika
Version:
Optics: modular data access for JavaScript
539 lines (433 loc) • 12.5 kB
JavaScript
/**
# Optika
<img src="https://raw.githubusercontent.com/phadej/optika/master/optika-300.png" align="right" height="100" />
> Optics: modular data access
[](http://travis-ci.org/phadej/optika)
[](http://badge.fury.io/js/optika)
[](https://david-dm.org/phadej/optika)
[](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)
);
```
*/
;
/**
## 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