Security News
The Risks of Misguided Research in Supply Chain Security
Snyk's use of malicious npm packages for research raises ethical concerns, highlighting risks in public deployment, data exfiltration, and unauthorized testing.
[ Tutorial | Reference | Related Work ]
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.
Playing with zippers in a REPL can be very instructive. First we require the
libraries and define a little helper using
reduce
to perform a sequence of
operations on a value:
const R = require("ramda")
const F = require("fastener")
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
. However, you should
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: [],
// focus:
// [ { language: 'en', text: 'Title' },
// { language: 'sv', text: 'Rubrik' } ],
// right: [],
// keys: [ 'contents' ],
// up: {} }
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 item of contents
using F.downHead
:
seq(F.toZipper(data),
F.downTo('contents'),
F.downHead)
// { left: [],
// focus: { language: 'en', text: 'Title' },
// right: [ { language: 'sv', text: 'Rubrik' } ],
// up: { left: [], right: [], keys: [ 'contents' ], up: {} } }
And continue into the first item of that which happens to the language
:
seq(F.toZipper(data),
F.downTo('contents'),
F.downHead,
F.downHead)
// { left: [],
// focus: 'en',
// right: [ 'Title' ],
// keys: [ 'language', 'text' ],
// up:
// { left: [],
// right: [ [Object] ],
// up: { left: [], right: [], keys: [Object], up: {} } } }
And to the next item, title
, using F.right
:
seq(F.toZipper(data),
F.downTo('contents'),
F.downHead,
F.downHead,
F.right)
// { left: [ 'en' ],
// focus: 'Title',
// right: [],
// keys: [ 'language', 'text' ],
// up:
// { left: [],
// right: [ [Object] ],
// up: { left: [], right: [], keys: [Object], up: {} } } }
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: [ 'en' ],
// focus: 'The Title',
// right: [],
// keys: [ 'language', 'text' ],
// up:
// { left: [],
// right: [ [Object] ],
// up: { left: [], right: [], keys: [Object], up: {} } } }
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)
// { focus: { language: 'en', text: 'The Title' },
// left: [],
// right: [ { language: 'sv', text: 'Rubrik' } ],
// up: { left: [], right: [], keys: [ 'contents' ], up: {} } }
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 pipeU = (...fs) => z => {
let r = z
for (let i=0; r !== undefined && i<fs.length; ++i)
r = fs[i](r)
return r
}
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 =>
pipeU(F.downTo('contents'),
find(r => r.language === language),
F.downTo('text'))
Now we can say:
pipeU(F.toZipper, textIn("en"), F.modify(x => 'The ' + x), F.fromZipper)(data)
// { 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.
The zipper combinators are available as named imports. 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.
F.toZipper(json)
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)
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 combinators allow one to inspect and modify the element that a zipper is focused on.
F.get(zipper)
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(fn, 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)
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 combinators can be applied to any zipper, but they return undefined
in case of illegal moves.
Parent-Child movement is moving the focus between that parent object or array and a child element of said parent.
F.downHead(zipper)
F.downHead(zipper)
moves the focus to the leftmost element of the object or
array that the zipper is focused on.
F.downLast(zipper)
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)
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)
F.keyOf(zipper)
returns the object property name or the array index that the
zipper is currently focused on.
F.up(zipper)
F.up(zipper)
moves the focus from an array element or object property to the
containing array or object.
Sibling movement is moving the focus between the elements of an array or an object.
F.head(zipper)
F.head(zipper)
moves the focus to the leftmost sibling of the current focus.
F.last(zipper)
F.last(zipper)
moves the focus to the rightmost sibling of the current focus.
F.left(zipper)
F.left(zipper)
moves the focus to the element on the left of the current focus.
F.right(zipper)
F.right(zipper)
moves the focus to the element on the right of the current focus.
F.queryMove(move, default, fn, zipper)
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
F.transformMove(move, fn, zipper)
F.transformMove(move, fn, zipper)
applies the given function to the zipper
focused on after the given movement. The function 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 originall 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(fn, 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 } ] }
While the implementation is very different, the choice of combinators is based on Michael D. Adams' paper Scrap Your Zippers.
FAQs
Functional Zipper for manipulating JSON
The npm package fastener receives a total of 7 weekly downloads. As such, fastener popularity was classified as not popular.
We found that fastener demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Snyk's use of malicious npm packages for research raises ethical concerns, highlighting risks in public deployment, data exfiltration, and unauthorized testing.
Research
Security News
Socket researchers found several malicious npm packages typosquatting Chalk and Chokidar, targeting Node.js developers with kill switches and data theft.
Security News
pnpm 10 blocks lifecycle scripts by default to improve security, addressing supply chain attack risks but sparking debate over compatibility and workflow changes.