import { UniversalWeakMap } from "./universal-weak-map.js";
import { UniversalWeakMap } from "./universal-weak-map.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
)));
};