Repository: yelouafi/focused Branch: master Commit: 71973cb893cd Files: 19 Total size: 47.3 KB Directory structure: gitextract_uambnlci/ ├── .eslintrc.json ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── index.d.ts ├── package.json ├── src/ │ ├── index.js │ ├── iso.js │ ├── lens.js │ ├── lensProxy.js │ ├── operations.js │ ├── prism.js │ ├── traversal.js │ ├── typeClasses.js │ └── utils.js └── test/ ├── curry.test.js ├── index.test.js └── optics.test.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc.json ================================================ { "extends": "eslint:recommended", "ecmaFeatures": { "modules": true, "spread": true, "restParams": true }, "env": { "browser": true, "node": true, "es6": true }, "parserOptions": { "ecmaVersion": 9, "sourceType": "module" }, "rules": { "no-unused-vars": ["error", { "argsIgnorePattern": "^_" }] } } ================================================ FILE: .gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # nyc test coverage .nyc_output # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # TypeScript v1 declaration files typings/ # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variables file .env # next.js build output .next # Node specific modules cjs/ ================================================ FILE: .npmignore ================================================ # dev-oly folders .babelrc* .eslintrc* .bookignore book.json test examples node_modules # doc folders test docs examples _book ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2018 Yassine Elouafi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # focused Yet another Optics library for JavaScript, based on the famous lens library from Haskell. Wrapped in a convenient Proxy interface. Put simply, this library will allow you to: - Create functional references (Optics), i.e. like pointers to nested parts in data structures (e.g. Object properties, Array elements, Map keys/values, or even fancier parts like a number inside a string ...). - Apply immutable updates to data structures pointed by those functional references. # Install ```js yarn add focused ``` or ```sh npm install --save focused ``` # Tutorial Lenses, or Optics in general, are an elegant way, from functional programming, to access and update immutable data structures. Simply put, an optic gives us a reference, also called a _focus_, to a nested part of a data structure. Once we build a focus (using some helper), we can use given functions to access or update, immutably, the embedded value. In the following tutorial, we'll introduce Optics using `focused` helpers. The library is meant to be friendly for JavaScript developers who are not used to FP jargon. We'll use the following object as a test bed ```js import { lensProxy, set, ... } from "focused"; const state = { name: "Luffy", level: 4, nakama: [ { name: "Zoro", level: 3 }, { name: "Sanji", level: 3 }, { name: "Chopper", level: 2 } ] }; // we'll use this as a convenient way to access deep properties in the state const _ = lensProxy(); ``` ## Focusing on a single value Here is our first example, using the `set` function: ```js const newState = set(_.name, "Mugiwara", state); // => { name: "Mugiwara", ... } ``` above, `set` takes 3 arguments: 1. `_.name` is a _Lens_ which lets us focus on the `name` property inside the `state` object 2. The new value which replaces the old one 3. The state to operate on. It then returns a new state, with the `name` property replaced with the new value. `over` is like `set` but takes a function instead of a constant value ```js const newState = over(_.level, x => x * 2, state); // => { name: "Luffy", level: 8, ... } ``` As you may have noticed, `set` is just a shortcut for `over(lens, _ => newValue, state)`. Besides properties, we can access elements inside an array ```js set(_.nakama[0].name, "Jimbi", state); ``` It's important to remember that a Lens focuses _exactly_ on 1 value. no more, no less. In the above example, accessing a non existing property on `state` (or out of bound index) will throw an error. If you want the access to silently fail, you can prefix the property name with `$`. ```js const newState = over(_.$assistant.$level, x => x * 2, state); // newState == state ``` `_.$assistant` is sometimes called an _Affine_, which is a focus on _at most_ one value (ie 0 or 1 value). There is also a `view` function, which provides a read only access to a Lens ```js view(_.name, state); // => Luffy ``` You're probably wondering, what's the utility of the above function, since the access can be trivially achieved with `state.name`. That's true, but Lenses allows more advanced accesses that are not as trivial to achieve as the above case, especially when combined with other Optics as we'll see. Similarly, `preview` can be used with Affines to safely dereference deeply nested values ```js preview(_.$assitant.$level, state); // null ``` ## Focusing on multiple values As we said, Lenses can focus on a single value. To focus on multiple values, we can use the `each` Optic together with `toList` function (`view` can only view a single value). For example, to gets the `name`s of all Luffy's `nakama` ```js toList(_.nakama.$(each).name, state); // => ["Zoro", "Sanji", "Chopper"] ``` Note how we wrapped `each` inside the `.$()` method of the proxy. `.$()` lets us insert arbitrary Optics in the access path which will be automatically composed with the other Optics in the chain. In Optics jargon, `each` is called a _Traversal_. It's an optic which can focus on multiple parts inside a data structure. Note that Traversals are not restricted to lists. You can create your own Traversals for any _Traversable_ data structure (eg Maps, trees, linked lists ...). Of course, Traversals work automatically with update functions like `over`. For example ```js over(_.nakama.$(each).name, s => s.toUpperCase(), state); ``` returns a new state with all `nakama` names uppercased. Another Traversal is `filtered` which can restrict the focus only to parts meeting some criteria. For example ```js toList(_.nakama.$(filtered(x => x.level > 2)).name, state); // => ["Zoro", "Sanji"] ``` retrieves all `nakama`s names with level above `2`. While ```js over(_.nakama.$(filtered(x => x.level > 2)).name, s => s.toUpperCase(), state); ``` updates all `nakama`s names with level above `2`. ## When the part and the whole matches Suppose we have the following json ```js const pkgJson = `{ "name": "my-package", "version": "1.0.0", "description": "Simple package", "main": "index.html", "scripts": { "start": "parcel index.html --open", "build": "parcel build index.html" }, "dependencies": { "mydep": "6.0.0" } } `; ``` And we want to focus on the `mydep` field inside `dependencies`. With normal JS code, we can call `JSON.parse` on the json string, modify the field on the created object, then call `JSON.stringify` on the same object to create the new json string. It turns out that Optics has got a first class concept for the above operations. When the whole (source JSON) and the part (object created by `JSON.parse`) _matches_ we call that an _Isomorphism_ (or simply _Iso_). In the above example we can create an Isomorphism between the JSON string and the corresponding JS object using the `iso` function ```js const json = iso(JSON.parse, JSON.stringify); ``` `iso` takes 2 functions: one to go from the source to the target, and the other to go back. > Note this is a partial Optic since `JSON.parse` can fail. We've got another Optic (oh yeah) to account for failure Ok, so having the `json` Iso, we can use it with the standard functions, for example ```js set(_.$(json).dependencies.mydep, "6.1.0", pkgJson); ``` returns another JSON string with the `mydep` modified. Abstracting over the parsing/stringifying steps. The previous example is nice, but it'd be nicer if we can get access to the semver string `6.0.0` as a regular JS object. Let's go a little further and create another Isomorphism for semver like strings ```js const semver = iso( s => { const [major, minor, patch] = s.split(".").map(x => +x); return { major, minor, patch }; }, ({ major, minor, patch }) => [major, minor, patch].join(".") ); ``` Now we can have a focus directly on the parts of a semver string as numbers. Below ```js over(_.$(json).dependencies.mydep.$(semver).minor, x => x + 1, jsonObj); ``` increments the minor directly in the JSON string. > Of course, we abstracted over failures in the semver Iso. ## When the match can't always succeed As I mentioned, the previous case was not a total Isomorphism because JSON strings aren't always parsed to JS objects. So, as you may expect, we need to introduce another fancy name, this time our Optic is called a `Prism`. Which is an Isomorphism that may fail when going from the source to the target (but which always succeeds when going back). A simple way to create a Prism is the `simplePrism` function. It's like `iso` but you return `null` when the conversion fails. ```js const maybeJson = simplePrism(s => { try { return JSON.parse(s); } catch (e) { return null; } }, JSON.stringify); ``` So now, something like ```js const badJSonObj = "@#" + jsonObj; set(_.$(maybeJson).dependencies.mydep, "6.1.0", badJSonObj); ``` will simply return the original JSON string. The conversion of the `semver` Iso to a Prism is left as an exercise. # Documentation Using Optics follows a uniform pattern - First we create an Optic which focuses on some value(s) inside a container - Then we use an operation to access or modify the value through the created Optic >In the following, all showcased functions are imported from the `focused` package ## Creating Optics As seen in the tutorial,`lensProxy` offers a convenient way to create Optics which focus on javascript objects and arrays. `lensProxy` is essentially a façade API which uses explicit functions behind the scene. In the following examples, we'll see both the proxy and the coresponding explicit functions. ### Object properties As we saw in the tutorial, we use the familiar property access notation to focus on an object property. For example ```js const _ = lensProxy() const nameProp = _.name ``` creates a Lens which focuses on the `name` property of an object. Using the explicit style, we can use the the `prop` function ```js const nameProp = prop("name") ``` As said previously, **a Lens focuses exactly on one value**, it means the value must exist in the target container (in this sense the `prop` lens is *partial*). For example, if you use `nameProp` on an object which doesn't have a `name` property, it will throw an error. ### Array elements As with object properties, we use the array index notation to focus on an array element at a specific index. For example ```js const _ = lensProxy() const firstElem = _[0] ``` creates a lens that focuses on the first element on an array. The underlying function is `index`, so we could also write ```js const firstElem = index(0) ``` `index` is also a partial Lens, meaning it will throw if given index is out of the bounds of the target array. ### Creating custom lenses The `lens` function can be used to create arbitrary Lenses. The function takes 2 parameters - `getter` is used to extract the focus value from the target container - `setter` is used to update the target container with a new focus value. In the following example, `nameProp` is equivalent to the `nameProp` Lens we saw earlier. ```js const nameProp = lens( s => s.name, (value, s) => ({...s, name: value}) ) ``` As you may have guessed, both `prop` and `index` can be implemented using `lens` ### Composing Lenses >Generally you can combine any 2 Optics together, even if they're of different kind (eg you can combine Lenses with Traversals) A nice property of Lenses, and Optics in general, is that they can be combined to create a focus on deeply nested values. For example ```js const _ = lensProxy() const street = _.freinds[0].address.street ``` creates a Lens which focuses on the `street` of the `address` of the first element of the `freinds` array. As a matter of comparaison, let's say we want to update, immutably, the `street` property on a given object `person`. Using JavaScript spread syntax ```js const firstFreind = person.freinds[0]; const newPerson = { ...person, freinds: [ { ...firstFreind, address: { ...firstFreind.address, street: "new street" } }, ...person.freinds.slice(1) ] }; ``` The equivalent operation in `focused` Lenses is ```js const newPerson = set(_.freinds[0].address.street, "new street", person) ``` We're chaining `.` accesses to successively focus on deeply nested values. Behind the scene, `lensProxy` is creating the necessary `prop` and `index` Lenses, then composing them using `compose` function. Using explicit style, the above Lens could be rewritten like ```js const streetLens = compose( prop("freinds"), index(0), prop("address"), prop("street") ); ``` The important thing to remember here, is that`lensProxy` is essentially doing the same thing in the above `compose` example. Plus some memoization tricks to ensure that Lenses are created only once and reused on subsequent operations. ## Creating Isomorphisms Isomorphisms, or simply Isos, are useful when we want to switch between different representations of the same object. In the tutorial, we already saw `json` which create an Iso between a JSON string and the underlying object that the string parses to. As we saw, we can use the `iso` function to create a simple Iso. It takes a couple of functions - the firs function is used to convert from the source representation to the target one - the second function is used to convert back We'll see another interesting example of Isos in the next section ## Creating Traversals While Lenses can focus exactly on one value. Traversals has the ability to focus on many values (including `0`). ### Array Traversals Perhaps the most familiar Traversal is `each` which focuses on all elements of an array ```js const todos = ["each", "pray", "love"]; over(each, x => x.toUpperCase(), todos) // ["EACH", "PRAY", "LOVE"] ``` which is essentially equivalent to the `map` operation of array. However, as we said, what sets Optics apart is their ability to compose with other Optics ```js const todos = [ { title: "eat", done: false }, { title: "pray", done: false }, { title: "love", done: false } ]; // set done to `true` for all todos set( compose(each, prop("done")), true, todos ) ``` This can be more concisely formulated using the proxy interface ```js const _ = lensProxy(); set(_.$(each).done, true, todos) ``` Note that when Traversals are composed with another Optic, the result is always a Traversal. ### Traversing Map's keys/values Another useful example is traversing keys or values of a JavaScript `Map` object. Although the library already provides `eachMapKey` and `eachMapValue` Traversals for that purpose, it would be instructive to see how we can build them by simple composition of more primitive Optics. First, we can observe that a `Map` object can be seen also as a collection of `[key, value]` pairs. So we can start by creating an Iso between `Map` and `Array<[key, value]>` ```js const mapEntries = iso( map => [...map.entries()], entries => new Map(entries) ); ``` Then from here, we can traverse keys or values by simply focusing on the appropriate index (`0` or `1`) of each pair in the returned array. ```js eachMapValue = compose(mapEntries, each, index(1)); eachMapKey = compose(mapEntries, each, index(0)); ``` Since composition with a Traversal is also a Traversal. In the above examples, we obtain, in both cases, a Traversal that focuses on all key/values of the Map. As an illustration, the following example use `eachMapValue` combined with the `prop("score")` lens to increase the score of each player stored in the Map. ```js const playerMap = new Map([ ["Yassine", { name: "Yassine", score: 41 }], ["Yahya", { name: "Yahya", score: 800 }], ["Ayman", { name: "Ayman", score: 410} ] ]); const _ = lensProxy(); over( _.$(eachMapValue).score, x => x + 1000, playerMap ); ``` ### Filtered Traversals Another useful function is `filtered`. It can be used to restrict the set of values obtained from another Traversal. The function takes 2 arguments - A predicate used to filter traversed elements - The Traversal to be filtered (defaults to `each`) ```js const todos = [ { title: "eat", done: false }, { title: "pray", done: true }, { title: "love", done: true } ]; const isDone = t => t.done // view title of all done todos toList(_.$(filtered(isDone)).title, todos); // => ["pray", "love"] // set done of all done todos to false set(_.$(filtered(isDone)).done, false, todos) ``` Note that `filtered` can work with arbitrary traversals, not just arrays. ```js const playersAbove300 = filtered(p => p.score > 300, eachMapValue) over( _.$(playersAbove300).score, x => x + 1000, playerMap ); ``` (TBC) ## Todos - [ ] API docs - [x] add typings - [ ] Indexed Traversals ================================================ FILE: index.d.ts ================================================ // convenient shortcut for functions taking 1 param export type Fn = (x: A) => B; export type Const = R & { _brand: A }; export type Either = | { type: "Left"; value: A } | { type: "Right"; value: B }; export interface Monoid { empty: () => A; concat: (xs: A[]) => A; } export interface Functor { map(f: Fn, x: FA): FB; } export interface Applicative extends Functor { pure: Fn; combine: (f: Fn, fas: FA[]) => FB; } export interface Getting { "~~type~~": "Getting"; "~~apply~~": , FS extends Const>( F: Applicative, f: Fn, s: S ) => FS; } export interface Getter { "~~type~~": "Getting"; "~~apply~~": , FS extends Const>( F: Functor, f: Fn, s: S ) => FS; } export interface Iso { "~~type~~": "Getting" & "Iso" & "Lens" & "Traversal"; "~~apply~~": ((F: Functor, f: Fn, s: S) => FT); from: (s: S) => A; to: (b: B) => T; } export interface Prism { "~~type~~": "Getting" & "Prism" & "Traversal"; "~~apply~~": (( F: Applicative, f: Fn, s: S ) => FT); match: (s: S) => Either; build: (b: B) => T; } export interface Lens { "~~type~~": "Getting" & "Lens" & "Traversal"; "~~apply~~": ((F: Functor, f: Fn, s: S) => FT); } export interface Traversal { "~~type~~": "Getting" & "Traversal"; "~~apply~~": (( F: Applicative, f: Fn, s: S ) => FT); } // Monomorphic version export type SimpleIso = Iso; export type SimplePrism = Prism; export type SimpleLens = Lens; export type SimpleTraversal = Traversal; // arity 2 export function compose( parent: Iso, child: Iso ): Iso; export function compose( parent: Prism, child: Prism ): Prism; export function compose( parent: Lens, child: Lens ): Lens; export function compose( parent: Traversal, child: Traversal ): Traversal; export function compose( parent: Getter, child: Getter ): Getter; // arity 3 export function compose( parent: Iso, child1: Iso, child2: Iso ): Iso; export function compose( parent: Prism, child1: Prism, child2: Prism ): Prism; export function compose( parent: Traversal, child1: Traversal, child2: Traversal ): Traversal; export function compose( parent: Getter, child1: Getter, child2: Getter ): Getter; // arity 4 export function compose( parent: Iso, child1: Iso, child2: Iso, child3: Iso ): Iso; export function compose( parent: Prism, child1: Prism, child2: Prism, child3: Prism ): Prism; export function compose( parent: Lens, child1: Lens, child2: Lens, child3: Lens ): Lens; export function compose( parent: Traversal, child1: Traversal, child2: Traversal, child3: Traversal ): Traversal; export function compose( parent: Getter, child1: Getter, child2: Getter, child3: Getter ): Getter; // arity 5 export function compose( parent: Iso, child1: Iso, child2: Iso, child3: Iso, child4: Iso ): Iso; export function compose( parent: Prism, child1: Prism, child2: Prism, child3: Prism, child4: Prism ): Prism; export function compose( parent: Lens, child1: Lens, child2: Lens, child3: Lens, child4: Lens ): Lens; export function compose( parent: Traversal, child1: Traversal, child2: Traversal, child3: Traversal, child4: Traversal ): Traversal; export function compose( parent: Getter, child1: Getter, child2: Getter, child3: Getter, child4: Getter ): Getter; // for higher arities you can use _.$().$()... of nest compose calls export function over( optic: Traversal, updater: (a: A) => B, state: S ): T; export function set( optic: Traversal, value: B, state: S ): T; export function view(optic: Getting, state: S): A; export function preview( optic: Getting, state: S ): A | null; export function has(optic: Getting, state: S): boolean; export function toList(optic: Getting, state: S): A[]; export function append( optic: SimpleTraversal, item: A, state: S ): S; export function insertAt( optic: SimpleTraversal, index: number, item: A, state: S ): S; export function removeAt( optic: SimpleTraversal, index: number, state: S ): S; export function removeIf( optic: SimpleTraversal, f: (x: A) => boolean, state: S ): S; export function iso( from: (s: S) => A, to: (b: B) => T ): Iso; export function from(anIso: Iso): Iso; export function withIso( anIso: Iso, f: (from: Fn, to: Fn) => R ): R; export function non(x: A): SimpleIso; export function anon(x: A, f: Fn): SimpleIso; // TODO: json :: SimpleIso // TODO: mapEntries :: SimpleIso>, Array<[Key,Value]> export function lens( get: (s: S) => A, set: (b: B, s: S) => T ): Lens; export function prop(name: K): SimpleLens; export function index(i: number): SimpleLens<[A], A>; export function atProp( name: K ): SimpleLens; export function to(getter: (s: S) => A): Getter; export function eachOf(): SimpleTraversal; export const each: SimpleTraversal; export function filtered( f: (x: A) => Boolean, traversal?: Traversal ): Traversal; export function maybeProp( name: K ): SimpleTraversal; // TODO: eachValue :: SimpleTraversal, V> // TODO: eachKey :: SimpleTraversal, K> export function left(a: A): Either; export function rght(b: B): Either; export function prism( match: (s: S) => Either, build: (b: B) => T ): Prism; export function simplePrism( match: (s: S) => A | null, build: (a: A) => S ): SimplePrism; export function withPrism( aPrism: Prism, f: (match: (s: S) => Either, build: (b: B) => T) => R ): R; // TODO: maybeJson :: SimplePrism export type LensProxy = SimpleLens & (S extends object ? { [K in keyof S]: LensProxy } : {}) & { $(child: SimpleLens): LensProxy; $(child: SimpleTraversal): TraversalProxy; $(child: Getter): GetterProxy; }; export type TraversalProxy = SimpleTraversal & (S extends object ? { [K in keyof S]: TraversalProxy } : {}) & { $(child: SimpleTraversal): TraversalProxy; $(child: Getter): GetterProxy; }; export type GetterProxy = Getter & (S extends object ? { [K in keyof S]: GetterProxy } : {}) & { $(child: Getter): GetterProxy; }; export function lensProxy(parent?: SimpleLens): LensProxy; ================================================ FILE: package.json ================================================ { "name": "focused", "version": "0.7.2", "description": "Lens/Optics library for JavaScript", "module": "src/index.js", "main": "cjs/index.js", "typings": "./index.d.ts", "repository": "https://github.com/yelouafi/focused.git", "author": "Yassine Elouafi ", "license": "MIT", "keywords": [ "optic", "lens", "isomorphism", "prism", "traversal" ], "devDependencies": { "babel-cli": "^6.26.0", "babel-preset-env": "^1.7.0", "eslint": "^5.8.0", "esm": "^3.0.84", "faucet": "^0.0.1", "rimraf": "^2.6.2", "tape": "^4.9.1" }, "scripts": { "lint": "eslint src test", "test": "node -r esm test/index.test | faucet", "check": "npm run lint && npm run test", "clean": "rimraf cjs", "build": "npm run clean && babel src --out-dir cjs", "prepare": "npm run build", "prerelease": "npm run check && npm run prepare", "release:patch": "npm run prerelease && npm version patch && git push --follow-tags && npm publish", "release:minor": "npm run prerelease && npm version minor && git push --follow-tags && npm publish", "release:major": "npm run prerelease && npm version major && git push --follow-tags && npm publish" }, "babel": { "presets": [ [ "env", { "targets": { "node": "current" } } ] ] } } ================================================ FILE: src/index.js ================================================ import { curry3, curry4 } from "./utils"; import { over } from "./operations"; export { iso, from, withIso, non, anon, json, mapEntries, compose2Isos } from "./iso"; export { prop, index, lens, atProp } from "./lens"; export { prism, withPrism, simplePrism, left, right, maybeJson, compose2Prisms } from "./prism"; export { each, eachOf, filtered, maybeProp, eachMapKey, eachMapValue } from "./traversal"; export { view, preview, toList, has, over, set, compose, compose2 } from "./operations"; export { lensProxy, idLens } from "./lensProxy"; function _append(lens, x, s) { return over(lens, xs => xs.concat([x]), s); } function _insertAt(lens, index, x, s) { return over( lens, xs => { return xs .slice(0, index) .concat([x]) .concat(xs.slice(index)); }, s ); } function _removeIf(lens, p, s) { return over(lens, xs => xs.filter((x, i) => !p(x, i)), s); } function _removeAt(lens, index, s) { return removeIf(lens, (_, i) => i === index, s); } export const append = curry3(_append); export const insertAt = curry4(_insertAt); export const removeIf = curry3(_removeIf); export const removeAt = curry3(_removeAt); ================================================ FILE: src/iso.js ================================================ /* type Iso = (Functor, A => F, S) => F & { from: S => A, to: B => T } type SimpleIso = Iso */ // iso : (S => A, B => T) => Iso export function iso(from, to) { function isoFn(aFunctor, f, s) { return aFunctor.map(to, f(from(s))); } // escape hatch to avoid profunctors Object.assign(isoFn, { __IS_ISO: true, from, to }); return isoFn; } // withIso : (Iso, (S => A, B => T) => R) => R export function withIso(anIso, f) { return f(anIso.from, anIso.to); } // parent : Iso from: S => A, to: B => T // child : Iso from: A => X, to: Y => B // return : Iso from: S => X, to: Y => T export function compose2Isos(parent, child) { const { from: sa, to: bt } = parent; const { from: ax, to: yb } = child; return iso(s => ax(sa(s)), y => bt(yb(y))); } // from : Iso => Iso export function from(anIso) { return iso(anIso.to, anIso.from); } // non : a => SimpleIso without a, A> export function non(a) { return iso( //from : Maybe -> a m => (m === null ? a : m), //to : a -> Maybe x => (x === a ? null : x) ); } // non : (a, A -> boolean) => SimpleIso where pred(A) == false, a> export function anon(a, pred) { return iso(m => (m === null ? a : m), x => (pred(x) ? null : x)); } // json : SimpleIso export const json = iso(JSON.parse, JSON.stringify); // SimpleIso>, Array<[Key,Value]> export const mapEntries = iso(map => [...map.entries()], kvs => new Map(kvs)); ================================================ FILE: src/lens.js ================================================ /* type Lens = (Functor, A => F, S) => F type SimpleLens = Lens */ // lens : (S => A, (B,S) => T) => Lens export function lens(getter, setter) { return function gsLens(aFunctor, f, s) { return aFunctor.map(a2 => setter(a2, s), f(getter(s))); }; } // prop : K => SimpleLens // PARTIAL : will throw if name isn't a prop of the target export function prop(name) { return lens( s => { if (!s.hasOwnProperty(name)) { throw new Error( `object ${JSON.stringify(s)} doesn't have property ${name}` ); } return s[name]; }, (a, s) => Object.assign({}, s, { [name]: a }) ); } // index : number => SimpleLens<[A], A> // PARTIAL : will throw if idx is out of bounds export function index(idx) { return function indexLens(aFunctor, f, xs) { if (idx < 0 || idx >= xs.length) { throw new Error("index out of bounds!"); } return aFunctor.map(a2 => { const ys = xs.slice(); ys[idx] = a2; return ys; }, f(xs[idx])); }; } // atProp : K => SimpleLens, Maybe> export function atProp(key) { return function atKeyLens(aFunctor, f, s) { let a = s !== null && s.hasOwnProperty(key) ? s[key] : null; return aFunctor.map(a2 => { if (a2 === null) { if (a === null || s === null) return s; const copy = Object.assign({}, s); delete copy[key]; return copy; } else { return Object.assign({}, s, { [key]: a2 }); } }, f(a)); }; } // to : (S => A) => SimpleLens export function to(getter) { return lens(getter, () => { throw new Error("Can not modify the value of a getter"); }); } ================================================ FILE: src/lensProxy.js ================================================ import { prop, index } from "./lens"; import { maybeProp } from "./traversal"; import { compose2 } from "./operations"; // idLens : SimpleLens export function idLens(_, f, s) { return f(s); } /** * returns a Proxy object for easing creation & composition of optics * * const _ = lensProxy() * _.name <=> prop("name") * _.todo.title <=> compose(prop("todo"), prop("title")) * * For convenience, safe access to non existing is provided by perfixng the prop name with '$' * * _.$name <=> maybeProp("name") * * You can also insert other optics usin '$' method of the proxy lensProxy, for example * * _.todos.$(each).title <=> compose(prop("todos"), each, prop("title")) * * is a traversal that focuses on all titles of the todo array */ function getOrCreateLens(memo, parent, key) { let l = memo.get(key); if (l == null) { let child; const num = Number(key); if (String(num) === key) { child = index(num); } else if (key[0] === "$") { child = maybeProp(key.slice(1)); } else { child = prop(key); } l = lensProxy(compose2(parent, child)); memo.set(key, l); } return l; } export function lensProxy(parent = idLens) { const memo = new Map(); return new Proxy(() => {}, { get(target, key) { if (key === "$") { return child => { return lensProxy(compose2(parent, child)); }; } return getOrCreateLens(memo, parent, key); }, apply(target, thiz, [F, f, s]) { return parent(F, f, s); } }); } ================================================ FILE: src/operations.js ================================================ import { id, konst, curry2, curry3 } from "./utils"; import { ConstAny, ConstFirst, ConstList, ConstVoid, Identity, List } from "./typeClasses"; import { compose2Isos } from "./iso"; import { compose2Prisms } from "./prism"; /* type Settable = (Identity, A => Identity, S) => Identity type Getting = (Const, A => Const, S) => Const type Getter = Getting -- ie should work for any R */ // view : Getting => S => A export const view = curry2(function _view(aGetter, s) { return aGetter(ConstVoid, id, s); }); function _over(aSettable, f, s) { return aSettable(Identity, f, s); } // over : Settable => (A => B) => S => T export const over = curry3(_over); // set : Settable => B => S => T export const set = curry3(function _set(aSettable, v, s) { return _over(aSettable, konst(v), s); }); // toList : Getting<[A], S,A> => S => [A] export const toList = curry2(function toList(aGetting, s) { return aGetting(ConstList, List.pure, s); }); // preview : Getting => S => (A | null) export const preview = curry2(function _preview(aGetting, s) { return aGetting(ConstFirst, id, s); }); // has : (Getting, S) => Boolean export const has = curry2(function _has(aGetting, s) { return aGetting(ConstAny, konst(true), s); }); /** * Compose 2 optics, Abstarcting the constraints, the type can be seen as * * compose2 : (Optic, Optic) => Optic * * However, we need also to combine 2 Isos into an Iso and idem for Prisms * In Haskell this is acheived using type classes & Profunctors * * Here we're just inspecting types at runtime, it's ugly and less flexible but * works for our limited cases. Most notably, I don't want to introduce Profunctors * for performance reasons. */ export function compose2(parent, child) { // ad-hoc polymporphism FTW if (parent.__IS_ISO && child.__IS_ISO) { return compose2Isos(parent, child); } if (parent.__IS_PRISM && child.__IS_PRISM) { return compose2Prisms(parent, child); } return function composedOptics(F, f, s) { return parent(F, a => child(F, f, a), s); }; } export function compose(...ls) { return ls.reduce(compose2); } ================================================ FILE: src/prism.js ================================================ /* type Either = { type: "LEFT", value: T } | { type: "RIGHT", value: A } type Prism = (Applicative, A => F, S) => F & { __IS_PRISM: true, match: S => Either, to: B => T } type SimplePrism = Prism */ export function left(value) { return { type: "LEFT", value }; } export function right(value) { return { type: "RIGHT", value }; } // prism : (S => Either, B => T) => Prism export function prism(match, build) { function prismFn(anApplicative, f, s) { const result = match(s); if (result.type === "LEFT") return anApplicative.pure(result.value); const fa = f(result.value); return anApplicative.map(build, fa); } // escape hatch to avoid profunctors Object.assign(prismFn, { __IS_PRISM: true, build, match }); return prismFn; } // simplePrism : (S => Maybe, A => S) => SimplePrism export function simplePrism(match, build) { return prism(s => { const result = match(s); return result === null ? { type: "LEFT", value: s } : { type: "RIGHT", value: result }; }, build); } // withPrism : (Prism, (S => Either, B => T) => R) => R export function withPrism(aPrism, f) { return f(aPrism.match, aPrism.build); } // parent : Prism & { match: S => Either, to: B => T } // child : Prism & { match: A => Either, to: Y => B } // return : Prism & { match: S => Either, to: Y => T } export function compose2Prisms(parentL, childL) { const { match: sta, build: bt } = parentL; const { match: abx, build: yb } = childL; return prism( s => { const ta = sta(s); if (ta.type === "LEFT") return ta; const bx = abx(ta.value); if (bx.type === "RIGHT") return bx; return bt(bx.value); }, y => bt(yb(y)) ); } // json : SimplePrism export const maybeJson = simplePrism(s => { try { return JSON.parse(s); } catch (e) { return null; } }, JSON.stringify); ================================================ FILE: src/traversal.js ================================================ import { id } from "./utils"; import { compose } from "./operations"; import { mapEntries } from "./iso"; import { index } from "./lens"; /* type Traversal = (Applicative, A => F, S) => F type SimpleTraversal = Traversal */ // each : Traversal< Array, Array, A, B> export function each(anApplicative, f, xs) { return anApplicative.combine(id, xs.map(f)); } // eachOf: () => Traversal< Array, Array, A, B> // this of the convenience of typings since TypeScript doesn't // allow type parameters on non functions export function eachOf() { return each; } // filter : (A => Boolean) => Traversal< Array, Array, A, B> export function filtered(pred, traverse = each) { return function filterTraversal(anApplicative, f, s) { return traverse(anApplicative, update, s); function update(v) { return pred(v) ? f(v) : anApplicative.pure(v); } }; } // maybeProp :: K => SimpleTraversal // This is an Affine Traversal; ie focus on 0 or 1 value export function maybeProp(name) { return function propTraversal(anApplicative, f, s) { if (!s.hasOwnProperty(name)) { return anApplicative.pure(s); } return anApplicative.map(a2 => { return Object.assign({}, s, { [name]: a2 }); }, f(s[name])); }; } // eachValue :: SimpleTraversal, V> export const eachMapValue = compose( mapEntries, each, index(1) ); // eachKey :: SimpleTraversal, K> export const eachMapKey = compose( mapEntries, each, index(0) ); ================================================ FILE: src/typeClasses.js ================================================ export const Void = { empty: () => { throw "Void.empty! (you're likely using view with a Traversal, try preview or toList instead)"; }, concat: () => { throw "Void.concat! (you're likely using view with a Traversal, try preview or toList instead)"; } }; export const List = { empty: () => [], concat: xxs => [].concat(...xxs), pure: x => [x], map: (f, xs) => xs.map(f) }; export const First = { empty: () => null, concat2: (x1, x2) => (x1 !== null ? x1 : x2), concat: xs => xs.reduce(First.concat2, null) }; export const Any = { empty: () => false, concat2: (x1, x2) => x1 || x2, concat: xs => xs.reduce(Any.concat2, false) }; export const Identity = { pure: x => x, map: (f, x) => f(x), combine: (f, xs) => f(xs) }; export const Const = aMonoid => ({ pure: _ => aMonoid.empty(), map: (f, x) => x, combine: (_, xs) => aMonoid.concat(xs) }); export const ConstVoid = Const(Void); export const ConstList = Const(List); export const ConstFirst = Const(First); export const ConstAny = Const(Any); ================================================ FILE: src/utils.js ================================================ export const id = x => x; export const konst = x => _ => x; export function curry2(f) { return function curried2(x, y) { if (arguments.length >= 2) return f(x, y); return function curried2_1arg(y) { return f(x, y); }; }; } export function curry3(f) { return function curried3(x, y, z) { if (arguments.length >= 3) return f(x, y, z); if (arguments.length === 2) { return function curried3_2args(z) { return f(x, y, z); }; } return curry2(function curried3_1(y, z) { return f(x, y, z); }); }; } export function curry4(f) { return function curried4(w, x, y, z) { if (arguments.length >= 4) return f(w, x, y, z); if (arguments.length === 3) { return function curried4_3args(z) { return f(w, x, y, z); }; } if (arguments.length === 2) { return curry2(function curried4_2args(y, z) { return f(w, x, y, z); }); } return curry3(function curried4_1(x, y, z) { return f(w, x, y, z); }); }; } ================================================ FILE: test/curry.test.js ================================================ import test from "tape"; import { curry2, curry3, curry4 } from "../src/utils"; const add2 = curry2((x, y) => x + y); const add3 = curry3((x, y, z) => x + y + z); const add4 = curry4((w, x, y, z) => w + x + y + z); test("curry2", assert => { assert.equal(add2(1, 2), 3); assert.equal(add2(1)(2), 3); assert.end(); }); test("curry3", assert => { assert.equal(add3(1, 2, 3), 6); assert.equal(add3(1, 2)(3), 6); assert.equal(add3(1)(2, 3), 6); assert.equal(add3(1)(2)(3), 6); assert.end(); }); test("curry4", assert => { assert.equal(add4(1, 2, 3, 4), 10); assert.equal(add4(1, 2, 3)(4), 10); assert.equal(add4(1, 2)(3, 4), 10); assert.equal(add4(1, 2)(3)(4), 10); assert.equal(add4(1)(2, 3, 4), 10); assert.equal(add4(1)(2, 3)(4), 10); assert.equal(add4(1)(2)(3, 4), 10); assert.equal(add4(1)(2)(3)(4), 10); assert.end(); }); ================================================ FILE: test/index.test.js ================================================ import "./curry.test"; import "./optics.test"; ================================================ FILE: test/optics.test.js ================================================ import test from "tape"; import { iso, maybeJson, view, over, toList, preview, set, each, filtered, json, lensProxy, anon, atProp, compose, append, insertAt, removeIf, removeAt } from "../src"; const state = { name: "Luffy", level: 4, nakama: [ { name: "Zorro", level: 3 }, { name: "Sanji", level: 3 }, { name: "Chooper", level: 2 } ] }; const _ = lensProxy(); test("view/prop", assert => { assert.equal(view(_.name, state), "Luffy"); assert.end(); }); test("preview/maybeProp", assert => { assert.deepEqual(preview(_.$lastname.level, state), null); assert.end(); }); test("over/maybeProp", assert => { assert.deepEqual(over(_.$assitant.$level, x => x * 2, state), state); assert.end(); }); test("over/prop", assert => { assert.deepEqual(over(_.level, x => x * 2, state), { ...state, level: state.level * 2 }); assert.end(); }); test("view/atProp", assert => { assert.equal( view(atProp("surname"), { name: "Luffy" }), null, "should return null if property is absent" ); assert.end(); }); test("view/atProp", assert => { assert.deepEqual( set(atProp("surname"), "Monkey D.", { name: "Luffy" }), { name: "Luffy", surname: "Monkey D." }, "Should add the property if absent" ); assert.deepEqual( set( compose( atProp("navigator"), atProp("name") ), "Nami", { name: "Luffy" } ), { name: "Luffy", navigator: { name: "Nami" } }, "Should add deeply nested property if absent" ); assert.end(); }); // _.at('address').at('street') // Lens<{}, Maybe<{}>> . Lens<{}, Maybe> test("toList/each", assert => { assert.deepEqual( toList(_.nakama.$(each).name, state), state.nakama.map(n => n.name) ); assert.end(); }); test("toList/filtered", assert => { assert.deepEqual( toList(_.nakama.$(filtered(x => x.level > 2)).name, state), state.nakama.filter(n => n.level > 2).map(n => n.name) ); assert.end(); }); test("over/filtered", assert => { assert.deepEqual( over(_.nakama.$(filtered(x => x.level > 2)).name, s => `**${s}**`, state), { ...state, nakama: state.nakama.map(n => n.level > 2 ? { ...n, name: `**${n.name}**` } : n ) } ); assert.end(); }); test("set/[0]", assert => { assert.deepEqual(set(_.nakama[0].name, "Jimbi", state), { ...state, nakama: state.nakama.map((n, i) => (i === 0 ? { ...n, name: "Jimbi" } : n)) }); assert.end(); }); test("iso/anon", assert => { // nonZero is an Iso from Maybe to number (with 1 as default value) // source is either null or a negative number // target is any number const negative = anon(0, x => x >= 0); assert.equal(view(negative, -10), -10); assert.equal(view(negative, null), 0); assert.equal( set(negative, -3, -10), -3, "should allow setting to negative numbers" ); assert.equal( set(negative, -3, null), -3, "should allow setting on null values" ); assert.equal( set(negative, 10, -3), null, "should not allow setting non-negative numbers" ); assert.end(); }); const jsonObj = `{ "name": "my-package", "version": "1.0.0", "description": "Simple package", "main": "index.html", "scripts": { "start": "parcel index.html --open", "build": "parcel build index.html" }, "dependencies": { "mydep": "6.0.0" } } `; const semver = iso( s => { const [major, minor, patch] = s.split(".").map(x => +x); return { major, minor, patch }; }, ({ major, minor, patch }) => [major, minor, patch].join(".") ); test("over/iso", assert => { const actualJSON = over( _.$(json).dependencies.mydep.$(semver).minor, x => x + 1, jsonObj ); const js = JSON.parse(jsonObj); js.dependencies.mydep = "6.1.0"; assert.deepEqual(JSON.parse(actualJSON), js); assert.end(); }); test("over/prism", assert => { const badJSonObj = "@#" + jsonObj; assert.equal( set(_.$(maybeJson).dependencies.mydep, "6.1.0", badJSonObj), badJSonObj ); assert.end(); }); test("append", assert => { assert.deepEqual(append(_.nakama, { name: "Nami", level: 1 }, state), { ...state, nakama: state.nakama.concat({ name: "Nami", level: 1 }) }); assert.end(); }); test("insertAt", assert => { assert.deepEqual(insertAt(_.nakama, 1, { name: "Nami", level: 1 }, state), { ...state, nakama: [ state.nakama[0], { name: "Nami", level: 1 }, ...state.nakama.slice(1) ] }); assert.end(); }); test("removeIf", assert => { assert.deepEqual(removeIf(_.nakama, n => n.level > 2, state), { ...state, nakama: state.nakama.filter(n => n.level <= 2) }); assert.end(); }); test("removeAt", assert => { assert.deepEqual(removeAt(_.nakama, 2, state), { ...state, nakama: state.nakama.filter((_, i) => i !== 2) }); assert.end(); });