Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/tommikaikkonen/zippa

A generic zipper library for JavaScript
https://github.com/tommikaikkonen/zippa

Last synced: 3 months ago
JSON representation

A generic zipper library for JavaScript

Awesome Lists containing this project

README

        

zippa
==============

**Generic [zipper](https://en.wikipedia.org/wiki/Zipper_(data_structure)) library for JavaScript with functional visitor utilities.**

The zippa core is similar to [Neith](https://github.com/mattbierner/neith), but implemented with JS Arrays instead infinite streams, and offers chainable API in addition to a functional API. The JS Array implementation should be more performant for data structures with low fan-out.

Functional visiting is implemented in the powerful `visit(visitors, initialState, zipper)` function, which visits the structure in depth-first order without recursion, maintaining a visit state, allowing breaking out of the visit, skipping subtrees, and handling pre and post visits separately for each element. `walk`, `preWalk` and `postWalk` are implemented using `visit`.

Inspired by:

- [`clojure.zip`](https://clojure.github.io/clojure/clojure.zip-api.html)
- [Tree visitors in Clojure](http://www.ibm.com/developerworks/library/j-treevisit/)
- [`zip-visit`](https://github.com/akhudek/zip-visit)
- [`Neith`](https://github.com/mattbierner/neith)

## Example Usage with ArrayZipper

Instantiation
```javascript
import { zip, ArrayZipper } from 'zippa';

let z = ArrayZipper.from([1, 2, 3, 4]);
```

The functional API is simple. You can import the functions straight from `zippa` with `import { up, down, right, left } from 'zippa'`, or get them under a namespace with `import { zip } from 'zippa';` All functions are curried.

```javascript
import pipe from 'ramda/src/pipe'; // reverse compose
import until from 'ramda/src/until';

const thirdChild = pipe(zip.down, zip.right, zip.right, zip.value);
thirdChild(z);
// 3

const increment = zip.edit(x => x + 1);
const incrementChildren = pipe(
z.down,
until(zip.isRightmost, pipe(increment, zip.right))
increment,
zip.root,
zip.value
);

incrementChildren(ArrayZipper.from([1, 2, 3, 4]));
// [2, 3, 4, 5]
```

The chainable API works with the identically named methods on the zipper value. You can exchange between between both styles. Methods are not curried.

```javascript
let z = ArrayZipper.from([1, 2, 3, 4]);
z = z.down();
z.value()
// 1
z = z.right();
z.value()
// 2
z = z.rightmost();
z.value()
// 4
z = z.remove();

z.root().value()
// [1, 2, 3]
```

## Example: Making a Tree Zipper

Zippers are very useful for functional modification of trees. Making a Zipper for a k-ary tree is simple.

First we define the tree node:

```javascript
class Node {
constructor(value, children) {
this.value = value;
this.children = children;
}
}
```

Then write the three functions required by `makeZipper` to make a concrete Zipper class:

```javascript
function isBranch(node) {
return node.children && !!node.children.length;
}

function getChildren(node) {
return node.children;
}

function makeNode(oldParent, children) {
return new Node(oldParent.value, children);
}
```

Import `zippa`, make a `TreeZipper`, and zip away.

```javascript
import { makeZipper } from 'zippa';

const TreeZipper = makeZipper(isBranch, getChildren, makeNode);

const tree = new Node(
1,
[
new Node(2),
new Node(3)
]
);

const z = TreeZipper.from(tree);

z.value().value
// 1

z.down().right().value().value
// 3
```

## Example: Implementing reduce for TreeZipper with `visit`

For reduce, we want to visit each node in the tree, keep a reference to an `acc` value, and call `reducerFn(acc, node)` on each node. You could do it without `zippa.visit`, but this is a good demonstration of `visit` usage.

`reduce`'s function signature looks like this:

```javascript
declare function reduce(fn: (acc: T, item: any) => T, initialAcc: any, zipper: Zipper): T;
```

The function passed to reduce takes `acc` first and the current item second, while a `zippa` visitor function takes an event type first (`zippa.PRE` or `zippa.POST`), item second and state third. To change the state of a visit, the visitor needs to return an object describing the changes. We want to update the state, so we have to return an object with the `state` key, whose value is the new state.

To make a visitor function out of a reducer function, we make a higher order function that returns a function conforming to the visitor interface. We also utilize `zippa.onPre`:

```javascript
import { onPre } from 'zippa';
const makeReduceVisitor = fn => onPre((item, state) => ({ state: fn(state, item) }));
```

`onPre(g)` returns a function that calls `g` with `item` and `state` only on the `PRE` event, so we don't need to deal with the event identifiers ourselves.

Now that we have a way to transform a reducer function to a visitor function, we'll need to pass the visitor function to `zippa.visit`. `zippa.visit` takes a list of visitor functions, an initial state, and a zipper value as arguments.

```javascript
import { visit } from 'zippa';

export const reduce = (fn, initialAcc, zipper) => {
const {
state,
item,
zipper
} = visit(
[makeReduceVisitor(fn)],
initialAcc,
zipper
);

return state;
}
```

Using the reduce function, we can gather all the data in our tree:

```javascript
const tree = new Node(
1,
[
new Node(2),
new Node(3)
]
);

const z = TreeZipper.from(tree);
const numbers = reduce((acc, node) => acc.concat(node.value), [], z);
// [1, 2, 3]
```

## API

[See API reference here](http://tommikaikkonen.github.io/zippa/).

## License

MIT. See `LICENSE`