GithubHelp home page GithubHelp logo

benjamn / immutable-tuple Goto Github PK

View Code? Open in Web Editor NEW
35.0 3.0 5.0 351 KB

Immutable finite list objects with constant-time equality testing (===) and no memory leaks.

Home Page: https://benjamn.github.io/immutable-tuple

License: MIT License

Shell 0.87% JavaScript 99.13%
immutable immutability tuple tuples persistent persistent-data-structure functional functional-programming internalization memoization

immutable-tuple's Introduction

immutable-tuple Build Status

Immutable finite list objects with constant-time equality testing (===) and no memory leaks.

Installation

First install the package from npm:

npm install immutable-tuple

or clone it from GitHub and then run npm install to compile the source code:

git clone https://github.com/benjamn/immutable-tuple.git
cd immutable-tuple
npm install
npm test # if skeptical

Usage

This package exports a single function called tuple, both as a default export and as an equivalent named export, so all of the following import styles will work:

import tuple from "immutable-tuple";
import { tuple } from "immutable-tuple";
const { tuple } = require("immutable-tuple");
const tuple = require("immutable-tuple").tuple;

Constructing tuples

The tuple function takes any number of arguments and returns a unique, immutable object that inherits from tuple.prototype and is guaranteed to be === any other tuple object created from the same sequence of arguments:

import assert from "assert";

const obj = { asdf: 1234 };
const t1 = tuple(1, "asdf", obj);
const t2 = tuple(1, "asdf", obj);

assert.strictEqual(t1 === t2, true);
assert.strictEqual(t1, t2);

Although the tuple function can be invoked using new tuple(...) syntax, using new is not recommended, since the new object will simply be thrown away.

Own tuple properties

The tuple object has a fixed numeric length property, and its elements may be accessed using array index notation:

assert.strictEqual(t1.length, 3);

t1.forEach((x, i) => {
  assert.strictEqual(x, t2[i]);
});

Nested tuples

Since tuple objects are just another kind of JavaScript object, naturally tuples can contain other tuples:

assert.strictEqual(
  tuple(t1, t2),
  tuple(t2, t1)
);

assert.strictEqual(
  tuple(1, t2, 3)[1][2],
  obj
);

However, because tuples are immutable and always distinct from any of their arguments, it is not possible for a tuple to contain itself, nor to contain another tuple that contains the original tuple, and so forth.

Constant time === equality

Since tuple objects are identical when (and only when) their elements are identical, any two tuples can be compared for equality in constant time, regardless of how many elements they contain.

This behavior also makes tuple objects useful as keys in a Map, or elements in a Set, without any extra hashing or equality logic:

const map = new Map;

map.set(tuple(1, 12, 3), {
  author: tuple("Ben", "Newman"),
  releaseDate: Date.now()
});

const version = "1.12.3";
const info = map.get(tuple(...version.split(".").map(Number)));
if (info) {
  console.log(info.author[1]); // "Newman"
}

Array methods

Every non-destructive method of Array.prototype is supported by tuple.prototype, including sort and reverse, which return a modified copy of the tuple without altering the original:

assert.strictEqual(
  tuple("a", "b", "c").slice(1, -1),
  tuple("b")
);

assert.strictEqual(
  tuple(6, 2, 8, 1, 3, 0).sort(),
  tuple(0, 1, 2, 3, 6, 8)
);

assert.strictEqual(
  tuple(1).concat(2, tuple(3, 4), 5),
  tuple(1, 2, 3, 4, 5)
);

Shallow immutability

While the identity, number, and order of elements in a tuple is fixed, please note that the contents of the individual elements are not frozen in any way:

const obj = { asdf: 1234 };
tuple(1, "asdf", obj)[2].asdf = "oyez";
assert.strictEqual(obj.asdf, "oyez");

Iterability

Every tuple object is array-like and iterable, so ... spreading and destructuring work as they should:

func(...tuple(a, b));
func.apply(this, tuple(c, d, e));

assert.deepEqual(
  [1, ...tuple(2, 3), 4],
  [1, 2, 3, 4]
);

assert.strictEqual(
  tuple(1, ...tuple(2, 3), 4),
  tuple(1, 2, 3, 4)
);

const [a, [_, b]] = tuple(1, tuple(2, 3), 4);
assert.strictEqual(a, 1);
assert.strictEqual(b, 3);

tuple.isTuple(value)

Since the immutable-tuple package could be installed multiple times in an application, there is no guarantee that the tuple constructor or tuple.prototype will be unique, so value instanceof tuple is unreliable. Instead, to test if a value is a tuple, you should use tuple.isTuple(value).

Fortunately, even if your application uses multiple different tuple constructors from different copies of this library, the resulting tuple instances will still be === each other when their elements are the same. This is especially convenient given that this library provides both a CommonJS bundle and an ECMAScript module bundle, and some module systems might accidentally load those bundles simultaneously.

Implementation details

Thanks to Docco, you can read my implementation comments side-by-side with the actual code by visiting the GitHub pages site for this repository.

Instance pooling (internalization)

Any data structure that guarantees === equality based on structural equality must maintain some sort of internal pool of previously encountered instances.

Implementing such a pool for tuples is fairly straightforward (though feel free to give it some thought before reading this code, if you like figuring things out for yourself):

const pool = new Map;

function tuple(...items) {
  let node = pool; 

  items.forEach(item => {
    let child = node.get(item);
    if (!child) node.set(item, child = new Map);
    node = child;
  });

  // If we've created a tuple instance for this sequence of elements before,
  // return that instance again. Otherwise create a new immutable tuple instance
  // with the same (frozen) elements as the items array.
  return node.tuple || (node.tuple = Object.create(
    tuple.prototype,
    Object.getOwnPropertyDescriptors(Object.freeze(items))
  ));
}

This implementation is pretty good, because it requires only linear time (O(items.length)) to determine if a tuple has been created previously for the given items, and you can't do better than linear time (asymptotically speaking) because you have to look at all the items.

This code is also useful as an illustration of exactly how the tuple constructor behaves, in case you weren't satisfied by my examples in the previous section.

Garbage collection

The simple implementation above has a serious problem: in a garbage-collected language like JavaScript, the pool itself will retain references to all tuple objects ever created, which prevents tuple objects and their elements (which may be very large objects) from ever being reclaimed by the garbage collector, even after they become unreachable by any other means. In other words, storing objects in this kind of tuple would inevitably cause memory leaks.

To solve this problem, it's tempting to try changing Map to WeakMap here:

const pool = new WeakMap;

and here:

if (!child) node.set(item, child = new WeakMap);

This approach is appealing because a WeakMap should allow its keys to be reclaimed by the garbage collector. That's the whole point of a WeakMap, after all. Once a tuple becomes unreachable because the program has stopped using it anywhere else, its elements are free to disappear from the pool of WeakMaps whenever they too become unreachable. In other words, something like a WeakMap is exactly what we need here.

Unfortunately, this strategy stumbles because a tuple can contain primitive values as well as object references, whereas a WeakMap only allows keys that are object references. In other words, node.set(item, ...) would fail whenever item is not an object, if node is a WeakMap. To see how the immutable-tuple library gets around this WeakMap limitation, have a look at this module.

Astute readers may object that some bookkeeping data remains in memory when you create tuple objects with prefixes of primitive values, but the important thing is that no user-defined objects are kept alive by the pool. That said, if you have any ideas for reclaiming chains of ._strongMap data, please open an issue or submit a pull request!

immutable-tuple's People

Contributors

benjamn avatar ptmuta avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

immutable-tuple's Issues

Expose Universal WeakMap

Hi!
UniversalWeakMap can be useful on its own, a good replacement for primitive-pool.
Can that possibly be exported as a separate entry?

import { UniversalWeakMap } from 'immutable-tuple'

"root" allocation is undefined on a polyfilled IE11

The following scenario happens in a polyfilled Angular 6+ application with Apollo and Apollo InMemoryCache when using IE11 and some versions of Safari.

The initial load of the package should create a new instance of the root node for the tree (here), but due to the non-standard polyfilled environment the globalKey in Array return true even through the value has not been defined within the Array object. The quick fix is to amend the in check with a definition check.

leaks when using primitives; fixable with WeakRef

The description claims "no leaks", but if you do Tuple(primitive), that creates a UniversalWeakMap which can never be garbage collected. If you're creating a lot of tuples with a lot of different primitives, that can be a problem.

Now that we have WeakRef and FinalizationRegistry, I'm pretty sure it's possible to do better, so that there's actually no leaks: that is, no matter how many tuples you create and how you create them, once they've all been GC'd you're guaranteed that no additional memory is held.

The necessary changes are:

  1. When set(key, value) is called on a UniversalWeakMap, store a WeakRef to value instead of storing value directly, and register value in a FinalizationRegistry so that when it is collected key is cleared from the map.
  2. Use a global WeakMap to store a map from each created tuple to all of its parent nodes.

Then the only strong references to UniversalWeakMap objects (except the root) are from the tuples stored in those maps. Once all the tuples stored in a given UniversalWeakMap are GC'd, it can be GC'd as well.

The only caveat with this approach is if the user uses a tuple as a key in a weak collection. In that case the tuple can still get collected even if it was only holding primitives, which might be surprising. But that's unavoidable if avoiding leaks.

module is not defined

Hi Ben, I don't get this error in React Native iOS or Android, but I do in Chrome with react-native-web. I don't know whether it's something you can publish an update to fix, or if I need to ask React Native or react-native-web to do an additional compilation step or something? 😄

$ npm ls immutable-tuple
/code/myapp
└─┬ [email protected]
 └─┬ [email protected]
   └─┬ [email protected]
     └── [email protected]
bundle.js:140912 Uncaught ReferenceError: module is not defined
   at Module.../../../../../../../../../code/myapp/node_modules/immutable-tuple/dist/tuple.mjs (bundle.js:140912)
   at __webpack_require__ (bundle.js:725)
   at fn (bundle.js:102)
   at Object.../../../../../../../../../code/myapp/node_modules/optimism/lib/index.js (bundle.js:174120)
   at __webpack_require__ (bundle.js:725)
   at fn (bundle.js:102)
   at Module.../../../../../../../../../code/myapp/node_modules/apollo-cache-inmemory/lib/optimism.js (bundle.js:47234)
   at __webpack_require__ (bundle.js:725)
   at fn (bundle.js:102)
   at Module.../../../../../../../../../code/myapp/node_modules/apollo-cache-inmemory/lib/readFromStore.js (bundle.js:47426)
   at __webpack_require__ (bundle.js:725)
   at fn (bundle.js:102)
   at Module.../../../../../../../../../code/myapp/node_modules/apollo-cache-inmemory/lib/inMemoryCache.js (bundle.js:46854)
   at __webpack_require__ (bundle.js:725)
   at fn (bundle.js:102)
   at Module.../../../../../../../../../code/myapp/node_modules/apollo-cache-inmemory/lib/index.js (bundle.js:47137)
   at __webpack_require__ (bundle.js:725)
   at fn (bundle.js:102)
   at Object.../../../../../../../../../code/myapp/node_modules/aws-appsync/lib/client.js (bundle.js:65093)
   at __webpack_require__ (bundle.js:725)
   at fn (bundle.js:102)
   at Object.../../../../../../../../../code/myapp/node_modules/aws-appsync/lib/index.js (bundle.js:66186)
   at __webpack_require__ (bundle.js:725)
   at fn (bundle.js:102)
   at Object.../../../../../../../../../code/myapp/node_modules/aws-appsync-react/lib/rehydrated.js (bundle.js:64799)
   at __webpack_require__ (bundle.js:725)
   at fn (bundle.js:102)
   at Object.../../../../../../../../../code/myapp/node_modules/aws-appsync-react/lib/index.js (bundle.js:64670)
   at __webpack_require__ (bundle.js:725)
   at fn (bundle.js:102)
   at Module.../../../../../../../../../code/myapp/App.js (bundle.js:824)
   at __webpack_require__ (bundle.js:725)
   at fn (bundle.js:102)
   at Module.../../../../../../../../../code/myapp/node_modules/expo/AppEntry.js (bundle.js:114414)
   at __webpack_require__ (bundle.js:725)
   at fn (bundle.js:102)
   at Object.0 (bundle.js:281583)
   at __webpack_require__ (bundle.js:725)
   at bundle.js:792
   at bundle.js:795

Action required: Greenkeeper could not be activated 🚨

🚨 You need to enable Continuous Integration on all branches of this repository. 🚨

To enable Greenkeeper, you need to make sure that a commit status is reported on all branches. This is required by Greenkeeper because it uses your CI build statuses to figure out when to notify you about breaking changes.

Since we didn’t receive a CI status on the greenkeeper/initial branch, it’s possible that you don’t have CI set up yet. We recommend using Travis CI, but Greenkeeper will work with every other CI service as well.

If you have already set up a CI for this repository, you might need to check how it’s configured. Make sure it is set to run on all new branches. If you don’t want it to run on absolutely every branch, you can whitelist branches starting with greenkeeper/.

Once you have installed and configured CI on this repository correctly, you’ll need to re-trigger Greenkeeper’s initial pull request. To do this, please delete the greenkeeper/initial branch in this repository, and then remove and re-add this repository to the Greenkeeper App’s white list on Github. You'll find this list on your repo or organization’s settings page, under Installed GitHub Apps.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.