• 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
      )));
    };