All Projects → polytypic → fastener

polytypic / fastener

Licence: MIT License
Functional Zipper for manipulating JSON

Programming Languages

javascript
184084 projects - #8 most used programming language

Projects that are alternatives of or similar to fastener

Typed
The TypeScript Standard Library
Stars: ✭ 124 (+129.63%)
Mutual labels:  immutable, functional
Pypika
PyPika is a python SQL query builder that exposes the full richness of the SQL language using a syntax that reflects the resulting query. PyPika excels at all sorts of SQL queries but is especially useful for data analysis.
Stars: ✭ 1,111 (+1957.41%)
Mutual labels:  query, functional
Jslt
JSON query and transformation language
Stars: ✭ 367 (+579.63%)
Mutual labels:  query, transform
Html
A Virtual DOM based templating-engine for PHP
Stars: ✭ 86 (+59.26%)
Mutual labels:  immutable, functional
js-data-structures
🌿 Data structures for JavaScript
Stars: ✭ 56 (+3.7%)
Mutual labels:  immutable, functional
Pyrsistent
Persistent/Immutable/Functional data structures for Python
Stars: ✭ 1,621 (+2901.85%)
Mutual labels:  immutable, functional
Yalinqo
Yet Another LINQ to Objects for PHP [Simplified BSD]
Stars: ✭ 400 (+640.74%)
Mutual labels:  query, functional
Fpp
Functional PHP Preprocessor - Generate Immutable Data Types
Stars: ✭ 282 (+422.22%)
Mutual labels:  immutable, functional
SQLiteHelper
🗄 This project comes in handy when you want to write a sql statement easily and smarter.
Stars: ✭ 57 (+5.56%)
Mutual labels:  query, cursor
grand central
State-management and action-dispatching for Ruby apps
Stars: ✭ 20 (-62.96%)
Mutual labels:  immutable, functional
Immutable Tuple
Immutable finite list objects with constant-time equality testing (===) and no memory leaks.
Stars: ✭ 29 (-46.3%)
Mutual labels:  immutable, functional
peds
Type safe persistent/immutable data structures for Go
Stars: ✭ 57 (+5.56%)
Mutual labels:  immutable, functional
Partial.lenses
Partial lenses is a comprehensive, high-performance optics library for JavaScript
Stars: ✭ 846 (+1466.67%)
Mutual labels:  immutable, functional
conjson
(conventional, consistent, conformative) JSON - A simple, functional, no-tags-required mechanism to handle and transform JSON representations of values, consistently.
Stars: ✭ 47 (-12.96%)
Mutual labels:  functional, transform
Phpfn
Functional PHP Toolstet: Centralized monorepository for all libraries
Stars: ✭ 19 (-64.81%)
Mutual labels:  immutable, functional
Arquero
Query processing and transformation of array-backed data tables.
Stars: ✭ 384 (+611.11%)
Mutual labels:  query, transform
Switzerland
🇨🇭Switzerland takes a functional approach to Web Components by applying middleware to your components. Supports Redux, attribute mutations, CSS variables, React-esque setState/state, etc… out-of-the-box, along with Shadow DOM for style encapsulation and Custom Elements for interoperability.
Stars: ✭ 261 (+383.33%)
Mutual labels:  immutable, functional
Typed Immutable
Immutable and structurally typed data
Stars: ✭ 263 (+387.04%)
Mutual labels:  immutable, functional
transmute
kind of like lodash but works with Immutable
Stars: ✭ 35 (-35.19%)
Mutual labels:  immutable, functional
babl
JSON templating on steroids
Stars: ✭ 29 (-46.3%)
Mutual labels:  immutable, functional

Fastener · GitHub stars npm

Zippers are a powerful abstraction for implementing arbitrary queries and transforms on immutable data structures and for step-by-step navigation and modification of data structures. This library implements a simple zipper designed for manipulating JSON data.

npm version Bower version Build Status Code Coverage

Contents

Tutorial

Playing with zippers in a REPL can be very instructive. First we require the libraries

import * as F from "fastener"
import * as R from "ramda"

and define a little helper using reduce to perform a sequence of operations on a value:

const seq = (x, ...fs) => R.reduce((x, f) => f(x), x, fs)

Let's work with the following simple JSON object:

const data = {contents: [{language: "en", text: "Title"},
                         {language: "sv", text: "Rubrik"}]}

First we just create a zipper using F.toZipper:

seq(F.toZipper(data))
// { focus: { contents: [ [Object], [Object] ] } }

As can be seen, the zipper is just a simple JSON object and the focus is the data object that we gave to F.toZipper. As long the data structure being manipulated is JSON, you can serialize and deserialize zippers as JSON. However, it is recommended that you use the zipper combinators to operate on zippers rather than rely on their exact format.

Let's then move into the contents property of the object using F.downTo:

seq(F.toZipper(data),
    F.downTo('contents'))
// { left: null,
//   focus:
//    [ { language: 'en', text: 'Title' },
//      { language: 'sv', text: 'Rubrik' } ],
//   key: 'contents',
//   right: null }

As seen above, the focus now has the contents array. We can use F.get to extract the value under focus:

seq(F.toZipper(data),
    F.downTo('contents'),
    F.get)
// [ { language: 'en', text: 'Title' },
//   { language: 'sv', text: 'Rubrik' } ]

Then we move into the first element of contents using F.downHead:

seq(F.toZipper(data),
    F.downTo('contents'),
    F.downHead)
// { left: null,
//   focus: { language: 'en', text: 'Title' },
//   key: 0,
//   right: [ null, { language: 'sv', text: 'Rubrik' } ],
//   up: { left: null, key: 'contents', right: null } }

And continue into the first property of that which happens to be the language:

seq(F.toZipper(data),
    F.downTo('contents'),
    F.downHead,
    F.downHead)
// { left: null,
//   focus: 'en',
//   key: 'language',
//   right: [ null, 'Title', 'text' ],
//   up:
//    { left: null,
//      key: 0,
//      right: [ null, [Object] ],
//      up: { left: null, key: 'contents', right: null } } }

And to the next property, title, using F.right:

seq(F.toZipper(data),
    F.downTo('contents'),
    F.downHead,
    F.downHead,
    F.right)
// { left: [ null, 'en', 'language' ],
//   focus: 'Title',
//   key: 'text',
//   right: null,
//   up:
//    { left: null,
//      key: 0,
//      right: [ null, [Object] ],
//      up: { left: null, key: 'contents', right: null } } }

Let's then use F.modify to modify the title:

seq(F.toZipper(data),
    F.downTo('contents'),
    F.downHead,
    F.downHead,
    F.right,
    F.modify(t => "The " + t))
// { left: [ null, 'en', 'language' ],
//   focus: 'The Title',
//   key: 'text',
//   right: null,
//   up:
//    { left: null,
//      key: 0,
//      right: [ null, [Object] ],
//      up: { left: null, key: 'contents', right: null } } }

When we now move outwards using F.up we can see the changed title become part of the data:

seq(F.toZipper(data),
    F.downTo('contents'),
    F.downHead,
    F.downHead,
    F.right,
    F.modify(t => "The " + t),
    F.up)
// { left: null,
//   key: 0,
//   right: [ null, { language: 'sv', text: 'Rubrik' } ],
//   up: { left: null, key: 'contents', right: null },
//   focus: { language: 'en', text: 'The Title' } }

We can also just move back to the root and get the updated data structure using F.fromZipper:

seq(F.toZipper(data),
    F.downTo('contents'),
    F.downHead,
    F.downHead,
    F.right,
    F.modify(t => "The " + t),
    F.fromZipper)
// { contents:
//    [ { language: 'en', text: 'The Title' },
//      { language: 'sv', text: 'Rubrik' } ] }

The above hopefully helped to understand how zippers work. However, it is important to realize that one typically does not use zipper combinators to create such a specific sequence of operations. One rather uses the zipper combinators to create new combinators that perform more complex operations directly.

Let's first define a zipper combinator that, given a zipper focused on an array, tries to focus on an element inside the array that satisfies a given predicate:

const find = R.curry((p, z) => F.downTo(R.findIndex(p, F.get(z)), z))

Like all the basic zipper movement combinators, F.downTo is a partial function that returns undefined in case the index is out of bounds. Let's define a simple function to compose partial functions:

const pipePartial = (...fs) => z => {
  for (let i=0; z !== undefined && i<fs.length; ++i)
    z = fs[i](z)
  return z
}

We can now compose a zipper combinator that, given a zipper focused on an object like data, tries to focus on the text element of an object with the given language inside the contents:

const textIn = language => pipePartial(
  F.downTo('contents'),
  find(R.whereEq({language})),
  F.downTo('text'))

Now we can say:

seq(data,
    F.toZipper,
    textIn("en"),
    F.modify(x => 'The ' + x),
    F.fromZipper)
// { contents:
//    [ { language: 'en', text: 'The Title' },
//      { language: 'sv', text: 'Rubrik' } ] }

Of course, this just scratches the surface. Zippers are powerful enough to implement arbitrary transforms on data structures. This can also make them more difficult to compose and reason about than more limited approaches such as lenses.

Reference

The zipper combinators are available as named exports. Typically one just imports the library as:

import * as F from "fastener"

In the following examples we will make use of the function

const seq = (x, ...fs) => R.reduce((x, f) => f(x), x, fs)

written using reduce that allows one to express a sequence of operations to perform starting from a given value.

Introduction and Elimination

F.toZipper(json) ~> zipper

F.toZipper(json) creates a new zipper that is focused on the root of the given JSON object.

For example:

seq(F.toZipper([1,2,3]),
    F.downHead,
    F.modify(x => x + 1),
    F.fromZipper)
// [ 2, 2, 3 ]

F.fromZipper(zipper) ~> json

F.fromZipper(zipper) extracts the modified JSON object from the given zipper.

For example:

seq(F.toZipper([1,2,3]),
    F.downHead,
    F.modify(x => x + 1),
    F.fromZipper)
// [ 2, 2, 3 ]

Focus

Focus combinators allow one to inspect and modify the element that a zipper is focused on.

F.get(zipper) ~> json

F.get(zipper) returns the element that the zipper is focused on.

For example:

seq(F.toZipper(1), F.get)
// 1
seq(F.toZipper(["a","b","c"]),
    F.downTo(2),
    F.get)
// 'c'

F.modify(json => json, zipper) ~> zipper

F.modify(fn, zipper) is equivalent to F.set(fn(F.get(zipper)), zipper) and replaces the element that the zipper is focused on with the value returned by the given function for the element.

For example:

seq(F.toZipper(["a","b","c"]),
    F.downTo(2),
    F.modify(x => x + x),
    F.fromZipper)
// [ 'a', 'b', 'cc' ]

F.set(json, zipper) ~> zipper

F.set(json, zipper) replaces the element that the zipper is focused on with the given value.

For example:

seq(F.toZipper(["a","b","c"]),
    F.downTo(1),
    F.set('lol'),
    F.fromZipper)
// [ 'a', 'lol', 'c' ]

Movement

Movement combinators can be applied to any zipper, but they return undefined in case of illegal moves.

Parent-Child movement

Parent-Child movement is moving the focus between a parent object or array and a child element of said parent.

F.downHead(zipper) ~> maybeZipper

F.downHead(zipper) moves the focus to the leftmost element of the object or array that the zipper is focused on.

F.downLast(zipper) ~> maybeZipper

F.downLast(zipper) moves the focus to the rightmost element of the object or array that the zipper is focused on.

F.downTo(key, zipper) ~> maybeZipper

F.downTo(key, zipper) moves the focus to the specified object property or array index of the object or array that the zipper is focused on.

F.keyOf(zipper) ~> maybeKey

F.keyOf(zipper) returns the object property name or the array index that the zipper is currently focused on.

F.up(zipper) ~> maybeZipper

F.up(zipper) moves the focus from an array element or object property to the containing array or object.

Path movement

Path movement is moving the focus along a path from a parent object or array to a nested child element.

F.downPath([...keys], zipper) ~> maybeZipper

F.downPath(path, zipper) moves the focus along the specified path of keys.

F.pathOf(zipper) ~> [...keys]

F.pathOf(zipper) returns the path from the root to the current element focused on by the zipper.

Sibling movement

Sibling movement is moving the focus between the elements of an array or an object.

F.head(zipper) ~> maybeZipper

F.head(zipper) moves the focus to the leftmost sibling of the current focus.

F.last(zipper) ~> maybeZipper

F.last(zipper) moves the focus to the rightmost sibling of the current focus.

F.left(zipper) ~> maybeZipper

F.left(zipper) moves the focus to the element on the left of the current focus.

F.right(zipper) ~> maybeZipper

F.right(zipper) moves the focus to the element on the right of the current focus.

Queries

F.queryMove(zipper => maybeZipper, value, zipper => value, zipper) ~> value

F.queryMove(move, default, fn, zipper) applies the given function fn to the zipper focused on after the given movement and returns the result unless the move was illegal in which case the given default value is returned instead.

For example:

seq(F.toZipper({x: 1}),
    F.queryMove(F.downTo('y'), false, () => true))
// false
seq(F.toZipper({y: 1}),
    F.queryMove(F.downTo('y'), false, () => true))
// true

Transforms

F.transformMove(move, zipper => zipper, zipper) ~> zipper

F.transformMove(move, fn, zipper) applies the given function to the zipper focused on after the given movement. The movement move must be one of F.downHead, F.downLast, F.downTo(key), F.left, F.right, or F.up. The function fn must the return a zipper focused on the same element that it was given. Then the focus is moved back to the element that the zipper was originally focused on. Nothing is done in case of an illegal move.

For example:

seq(F.toZipper({y: 1}),
    F.transformMove(F.downTo('y'), F.modify(x => x + 1)),
    F.fromZipper)
// { y: 2 }
seq(F.toZipper({x: 1}),
    F.transformMove(F.downTo('y'), F.modify(x => x + 1)),
    F.fromZipper)
// { x: 1 }

F.everywhere(json => json, zipper) ~> zipper

F.everywhere(fn, zipper) performs a transform of the focused element by modifying each possible focus of the element with a bottom-up traversal.

For example:

seq(F.toZipper({foo: 1,
                bar: [{lol: "bal", example: 2}]}),
    F.everywhere(x => typeof x === "number" ? x + 1 : x),
    F.fromZipper)
// { foo: 2, bar: [ { lol: 'bal', example: 3 } ] }

Related Work

While the implementation is very different, the choice of combinators is based on Michael D. Adams' paper Scrap Your Zippers.

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].