SkillAgentSearch skills...

Emerge

Use plain JS types as immutable data, with efficient merging and memory sharing

Install / Use

/learn @mitranim/Emerge
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Description

Utilities for using plain JavaScript dicts and lists as <a href="https://en.wikipedia.org/wiki/Immutable_object" target="_blank">immutable</a> data structures, with structural equality and memory-efficient updates using structural sharing.

JS and JSON have half-decent generic data structures, barring a few flaws:

  1. Updates always mutate in-place.
  2. No value equality, only reference equality.
  3. Only strings and symbols as dict keys.
  4. No sets, or no custom equality for ES2015 sets.
  5. No ordered dicts.
  6. Poor algorithmic complexity on list shift/unshift/splice.

Emerge addresses (1) and (2). It provides functions to "update" dicts and lists by creating new versions that share as much structure as possible with old versions. This is known as #structural sharing. It conserves memory and allows to use identity (#is) on sibling values as a fast substitute for "proper" value equality (#equal), which Emerge also provides.

Inspired by Clojure's ideas and the clojure.core data utils.

FP-friendly: only plain JS dicts and lists, no classes, no OO, bring your own data. Faster than all alternatives that I measured. Very lightweight (≈8 KiB un-minified), dependency-free. Written as one file with simple ES2015 exports. A good module bundler and minifier should drop out any functions you don't use.

Compatible with native JS modules.

TOC

Why

Why not ImmutableJS or something similar?

  1. Plain data. Emerge uses plain dicts and lists.
  • Uniform interface to data: read at path, set at path, merge. Just a few functions that work on all structures.
  • Easy to explore in a REPL.
  • No need for interop calls.
  • Complete compatibility with JSON.
  1. Size. At the time of writing, ImmutableJS is ≈ 57 KiB minified, unacceptable. Emerge is just a handful of KiB minified.

  2. Performance. Emerge is probably about as efficient as this kind of stuff gets.

Why not SeamlessImmutable?

SI is a popular library for merging and patching dicts and lists. Like Emerge, it sticks with plain JS data structures, and provides similar functions to Emerge.

At the time of writing, Emerge is way faster, more memory-efficient, and smaller than SI.

Installation

Node / Webpack

npm i -E emerge

Example usage:

import * as e from 'emerge'

e.put({one: 10}, 'two', 20)
// {one: 10, two: 20}

e.patch({one: 10}, {two: 20})
// {one: 10, two: 20}

e.remove({one: 10, two: 20}, 'two')
// {one: 10}

/* Structural sharing */

const prev = {one: [10], two: [20]}

// Patched version, keeping as much old structure as possible,
// even in the face of redundant overrides
const next = e.patch(prev, {one: [10], two: 20})
// {one: [10], two: 20}

// Unchanged values retain their references
next.one === prev.one  // true

Native Browser Modules

Emerge can be used as a native JS module in a browser:

import * as e from './node_modules/emerge/emerge.mjs'

Can use a CDN:

import * as e from 'https://cdn.jsdelivr.net/npm/emerge@0.5.1/emerge.mjs'

API

All examples on this page imply an import:

import * as e from 'emerge'

put(prev, key, value)

Similar to clojure.core/assoc.

Returns a data structure with value set at the given key. Works on dicts and lists. Also accepts null and undefined, treating them as {}. Rejects other operands.

Uses #structural sharing, may return the original input.

/* Dicts */

e.put({}, 'one', 10)
// {one: 10}

e.put({one: 10}, 'two', 20)
// {one: 10, two: 20}

/* Lists */

e.put([], 0, 'one')
// ['one']

e.put(['one'], 10, 'two')
// ['one', 'two']

/* Structural sharing */

const prev = {one: [10], two: [20]}

e.put(prev, 'two', [20]) === prev
e.put(prev, 'two', 20).one === prev.one

When putting into a list, the key must be an integer index within bounds, otherwise this produces an exception.

putIn(prev, path, value)

Similar to clojure.core/assoc-in. Like #put, but updates at a nested path rather than one key. Uses #structural sharing, may return the original input.

When path is []:

  • If prev is a primitive, returns value as-is, even if value is not a data structure.
  • If prev is a structure, performs a put-style deduplication, updating prev with the contents of value while preserving as many references as possible.

Otherwise, uses exactly the same rules as put:

  • Works for nested dicts and lists.
  • Creates nested dicts as needed.
  • Accepts null and undefined, treating them as {}.
  • When called with a non-empty path, rejects inputs other than null, undefined, a list, or a dict.
/* Dicts */

e.putIn({}, ['one'], 10)
// {one: 10}

e.putIn({one: 10}, ['one', 'two'], 20)
// {one: {two: 20}}

e.putIn(undefined, ['one'], 10)
// {one: 10}

/* Lists */

e.putIn([], [0], 'one')
// ['one']

e.putIn(['one', 'two'], [10], 'three')
// ['one', 'three']

/* Mixed */

e.putIn({one: [{two: 20}]}, ['one', 0, 'three'], 30)
// {one: [{two: 20, three: 30}]}

/* Structural sharing */

const prev = {one: [10], two: [20]}

e.putIn(prev, [], {one: [10], two: [20]}) === prev
e.putIn(prev, ['one'], [10]) === prev
e.putIn(prev, ['two'], 20).one === prev.one

putBy(prev, key, fun, ...args)

where fun: ƒ(prevValue, ...args)

Similar to #put, but takes a function and calls it with the previous value at the given key, passing the additional arguments, to produce the new value. Can be combined with other Emerge functions like #patch for great effect.

Uses #structural sharing, may return the original input.

e.putBy({one: {two: 20}}, 'one', e.patch, {three: 30})
// {one: {two: 20, three: 30}}

putInBy(prev, path, fun, ...args)

where fun: ƒ(prevValue, ...args)

Similar to #putIn and #putBy. Takes a function and calls it with the previous value at the given path, passing the additional arguments, to produce the new value. Can be combined with other Emerge functions like #patch for great effect. See putIn for the rules and examples.

Uses #structural sharing, may return the original input.

e.putInBy({one: {two: {three: 30}}}, ['one', 'two'], e.patch, {four: 4})
// {one: {two: {three: 30, four: 4}}}

patch(...dicts)

Takes any number of dicts and combines their properties. Ignores null and undefined inputs. Always produces a dict. Rejects other non-dict inputs.

Uses #structural sharing, may return the original input.

e.patch()
// {}

e.patch({one: 10}, {two: 20}, {three: 30})
// {one: 10, two: 20, three: 30}

// Ignores null and undefined operands
e.patch({one: 10}, undefined)
// {one: 10}

// Combines only at the top level
e.patch({one: {two: 20}}, {one: {three: 30}})
// {one: {three: 30}}

/* Structural sharing */

const prev = {one: [10], two: [20]}

e.patch(prev) === prev
e.patch(prev, {}) === prev
e.patch(prev, {one: [10]}) === prev
e.patch(prev, {one: [10], two: [20]}) === prev
e.patch(prev, {two: 200}).one === prev.one

merge(...dicts)

Same as #patch, but combines dicts at any depth:

Uses #structural sharing, may return the original input.

e.merge({one: {two: 20}}, {one: {three: 30}})
// {one: {two: 20, three: 30}}

insert(list, index, value)

Returns a version of list with value inserted at the given index. Index must be a natural number within the list's bounds + 1, which allows to insert or append elements. Going outside these bounds or providing an invalid index produces an exception.

Accepts null and undefined, treating them as []. Rejects other operands.

Uses #structural sharing, but never returns the original input because it always adds a new element. To update an existing element, use #put.

e.insert(undefined, 0, 'one')
// ['one']

e.insert([], 0, 'one')
// ['one']

e.insert(['one'], 1, 'two')
// ['one', 'two']

e.insert(['one', 'two'], 0, 'three')
// ['three', 'one', 'two']

remove(value, key)

Returns a version of value with the element at key removed. Works on dicts and lists. Accepts null and undefined, treating them as {}. Rejects other operands.

When value is a list, key must be an integer. Non-natural numbers such as 1.1 or -1 are ok and are simply ignored without removing an element.

Uses #structural sharing, may return the original input.

/* Dicts */

e.remove({one: 10, two: 20}, 'two')
// {one: 10}

e.remove({one: 10, two: 20}, 'three')
// {one: 10, two: 20}

/* Lists */

e.remove(['one', 'two', 'three'], 0)
// ['two', 'three']

e.remove(['one', 'two', 'three'], 1)
// ['one', 'three']
View on GitHub
GitHub Stars23
CategoryDevelopment
Updated2y ago
Forks4

Languages

JavaScript

Security Score

65/100

Audited on Mar 12, 2024

No findings