tuple.js

- Jump To … +
  src/tuple.js src/universal-weak-map.js src/util.js

- # tuple.js

- ¶

  See `universal-weak-map.js`.

      import { UniversalWeakMap } from "./universal-weak-map.js";

- ¶

  See `util.js`.

      import {
        brand,
        globalKey,
        forEachArrayMethod,
      } from "./util.js";

- ¶

  When called with any number of arguments, this function returns an object that inherits from `tuple.prototype` and is guaranteed to be `===` any other `tuple` object that has exactly the same items. In computer science jargon, `tuple` instances are “internalized” or just “interned,” which allows for constant-time equality checking, and makes it possible for tuple objects to be used as `Map` or `WeakMap` keys, or stored in a `Set`.

      export default function tuple(...items) {
        return intern(items);
      }

- ¶

  Named imports work as well as `default` imports.

      export { tuple };

- ¶

  If this package is installed multiple times, there could be mutiple implementations of the `tuple` function with distinct `tuple.prototype` objects, but the shared pool of `tuple` objects must be the same across all implementations. While it would be ideal to use the `global` object, there’s no reliable way to get the global object across all JS environments without using the `Function` constructor, so instead we use the global `Array` constructor as a shared namespace.

      const root = globalKey in Array
        ? Array[globalKey]
        : def(Array, globalKey, new UniversalWeakMap, false);

      function intern(array) {
        let node = root;

- ¶

  Because we are building a tree of weak maps, the tree will not prevent objects in tuples from being garbage collected, since the tree itself will be pruned over time when the corresponding `tuple` objects become unreachable. In addition to internalization, this property is a key advantage of the `immutable-tuple` package.

        array.forEach(item => {
          node = node.get(item) || node.set(item, new UniversalWeakMap);
        });

- ¶

  If a `tuple` object has already been created for exactly these items, return that object again.

        if (node.tuple) {
          return node.tuple;
        }

        const t = Object.create(tuple.prototype);

- ¶

  Define immutable items with numeric indexes, and permanently fix the `.length` property.

        array.forEach((item, i) => def(t, i, item, true));
        def(t, "length", array.length, false);

- ¶

  Remember this new `tuple` object so that we can return the same object earlier next time.

        return node.tuple = t;
      }

- ¶

  Convenient helper for defining hidden immutable properties.

      function def(obj, name, value, enumerable) {
        Object.defineProperty(obj, name, {
          value: value,
          enumerable: !! enumerable,
          writable: false,
          configurable: false
        });
        return 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)`.

      function isTuple(that) {
        return !! (that && that[brand] === true);
      }

      def(tuple.prototype, brand, true, false);

      tuple.isTuple = isTuple;

      function toArray(tuple) {
        const array = [];
        let i = tuple.length;
        while (i--) array[i] = tuple[i];
        return array;
      }

- ¶

  Copy all generic non-destructive Array methods to `tuple.prototype`. This works because (for example) `Array.prototype.slice` can be invoked against any `Array`-like object.

      forEachArrayMethod((name, desc, mustConvertThisToArray) => {
        const method = desc && desc.value;
        if (typeof method === "function") {
          desc.value = function (...args) {
            const result = method.apply(
              mustConvertThisToArray ? toArray(this) : this,
              args
            );

- ¶

  Of course, `tuple.prototype.slice` should return a `tuple` object, not a new `Array`.

            return Array.isArray(result) ? intern(result) : result;
          };
          Object.defineProperty(tuple.prototype, name, desc);
        }
      });

- ¶

  Like `Array.prototype.concat`, except for the extra effort required to convert any tuple arguments to arrays, so that

      tuple(1).concat(tuple(2), 3) === tuple(1, 2, 3)

      const { concat } = Array.prototype;
      tuple.prototype.concat = function (...args) {
        return intern(concat.apply(toArray(this), args.map(
          item => isTuple(item) ? toArray(item) : item
        )));
      };