Options
All
  • Public
  • Public/Protected
  • All
Menu

tuplerone

View this project on npm Travis Coveralls Dev Dependencies semantic-release Dependabot

A lightweight, efficient tuple and value object implementation for JavaScript and TypeScript.


A quick reminder about what tuples are (using Python):

(1, 2, 3) == (1, 2, 3) # → True

A JavaScript version of something similar looks like this:

'[1,2,3]' === '[1,2,3]'; // → true

Except it's using a string and would need to be unserialized with JSON.parse() to allow accessing the separate members. Moreover, JSON is limited in what values can be serialized.

You could alternatively use "1,2,3" and String.split(","), but it's also not very convenient. Just using an array doesn't work:

[1, 2, 3] === [1, 2, 3]; // → false

Each JavaScript array is a different object and so its value is the reference to that object. Tuples are a way to make that reference the same if the array members are the same. Using Tuplerone:

Tuple(1, 2, 3) === Tuple(1, 2, 3);

Example use case for tuples is dealing with memoization like React's memo() or PureComponent, since you can pass lists as props to components without forcing re-renders or manually caching the list. It's also useful for using multiple values as keys with Map(). In general, it's just a nice thing to have in your toolbox.

Try Tuplerone in a sandbox


This library is:

  • tiny (bundle size is under one kilobyte compressed), with no dependencies
  • well-typed using TypeScript (but can still be used from JavaScript, of course)
  • well-tested with full coverage
  • efficient using an ES2015 WeakMap-based directed acyclic graph for lookups

The Tuple objects are:

  • immutable – properties cannot be added, removed or changed, and it's enforced with Object.freeze()
  • array-like – tuple members can be accessed by indexing, and there's a length property, but no Array prototype methods
  • iterable – tuple members can be iterated over, for example, using for-of loops or spread syntax

There exists a stage-1 proposal for adding a tuple type to JavaScript and a different stage-1 proposal for adding a more limited value-semantic type.

Theory

Tuples are finite ordered sequences of values that serve two main purposes in programming languages:

  • grouping together heterogenous (mixed) data types within a static type system (this doesn't apply to a dynamically typed language like JavaScript)
  • simplifying value-semantic comparisons of lists, which is what this library is mainly about

Value semantics

A simple way to explain value semantics is to look at the difference between primitive values (like numbers and strings) and object values in JavaScript. Primitives are value-semantic by default, meaning that the default comparison methods (==, === and Object.is()) compare primitive values by their contents, so, for example, any string is equal to any other string created with the same contents:

'abc' === 'abc'; // → true, because both string literals create a value with the same contents

The contents of primitive values are also immutable (can't change at runtime), so the results of comparing primitive value equality can't be invalidated by the contents of the values changing.

Meanwhile, each object value (instance) in JavaScript has a unique identity, so each instance is only equal to itself and not any other instances:

[1, 2, 3] === [1, 2, 3]; // → false, because both array literals create separate array instances

Objects by default can't be thought of as their contents since the contents can change, and this is called reference semantics, since objects essentially represent a place in memory. The downside is that it makes reasoning about a program harder, since the programmer has to consider potential changes.

A more direct practical consequence of reference semantics is that comparing instances requires deep comparisons_, such as [`.isEqual()`]isequal in lodash or serializing the object values to JSON:

let a = [1, 2, 3];
let b = [1, 2, 3];
let result = JSON.stringify(a) === JSON.stringify(b); // → true, because it's a deep comparison
a.push(4); // a and b contents are now different, so the cached comparison result is invalid

Deep comparison results can't be reliably cached since the compared instances can change, and it's also less efficient than just being able to use === directly. An another thing that's not possible with reference semantics is combining different values to use as a composite key (such as with Map or WeakMap).

Directed acyclic graphs

Directed acyclic graphs (DAGs) are a data structure that allows efficiently mapping a sequence of values to a unique object containing them, which is how this library is implemented. Specifically, it uses a WeakMap object (optionally a Map as well if mapping primitives) for each node, and the nodes are re-used for overlapping paths in the graph. Map access has constant time complexity, so the number of tuples created doesn't slow down access speed. Using WeakMap ensures that if the values used to create the tuple are dereferenced, the tuple object gets garbage collected.

Installation

npm

npm install tuplerone

yarn

yarn add tuplerone

CDN

https://unpkg.com/tuplerone/dist/tuplerone.umd.js

Usage

Tuple(…values)

import { Tuple } from 'tuplerone';

// Dummy objects
const a = Object('a');
const b = Object('b');
const c = Object('c');

// Structural equality testing using the identity operator
Tuple(a, b, c) === Tuple(a, b, c); // → true
Tuple(a, b) === Tuple(b, a); // → false

// Mapping using a pair of values as key
const map = new Map();
map.set(Tuple(a, b), 123).get(Tuple(a, b)); // → 123

// Nesting tuples
Tuple(a, Tuple(b, c)) === Tuple(a, Tuple(b, c)); // → true

// Using primitive values
Tuple(1, 'a', a); // → Tuple(3) [1, "a", Object("a")]

// Indexing
Tuple(a, b)[1]; // → Object("b")

// Checking arity
Tuple(a, b).length; // → 2

// Failing to mutate
Tuple(a, b)[0] = c; // throws an error

The tuple function caches or memoizes its arguments to produce the same tuple object for the same arguments.

Types

The library is well-typed using TypeScript:

import { Tuple, Tuple0, Tuple1, Tuple2 } from 'tuplerone';

// Dummy object for use as key
const o = {};

const tuple0: Tuple0 = Tuple(); // 0-tuple
const tuple1: Tuple1<typeof o> = Tuple(o); // 1-tuple
const tuple2: Tuple2<typeof o, number> = Tuple(o, 1); // 2-tuple

Tuple(o) === Tuple(o, 1); // TS compile error due to different arities

// Spreading a TypeScript tuple:
Tuple(...([1, 2, 3] as const)); // → Tuple3<1, 2, 3>

In editors like VS Code, the type information is also available when the library is consumed as JavaScript.

CompositeSymbol(…values)

It's possible to avoid creating an Array-like tuple for cases where iterating the tuple members isn't needed (for example, just to use it as a key):

import { CompositeSymbol } from 'tuplerone';

typeof CompositeSymbol(1, 2, {}) === 'symbol'; // → true

A symbol is more space efficient than a tuple and can be used as a key for plain objects.

ValueObject(object)

Tuplerone also includes a simple value object implementation:

import { ValueObject } from 'tuplerone';

ValueObject({ a: 1, { b: { c: 2 } }}) === ValueObject({ a: 1, { b: { c: 2 } }}); // → true

Note that the passed objects are frozen with Object.freeze().

Caveats

Since this is a userspace implementation, there are a number of limitations.

At least one member must be an object to avoid memory leaks

Due to WeakMap being limited to using objects as keys, there must be at least one member of a tuple with the object type, or the tuples would leak memory. Trying to create tuples with only primitive members will throw an error.

Tuple(1, 2); // throws TypeError
Tuple(1, 2, {}); // works

WeakMap is an ES2015 feature which is difficult to polyfill (the polyfills don't support frozen objects), but this applies less to environments like node or browser extensions.

UnsafeTuple

There is an UnsafeTuple type for advanced use cases where the values not being garbage-collectable is acceptable, so it doesn't require having an object member:

import { UnsafeTuple as Tuple } from 'tuplerone';

Tuple(1, 2, 3) === Tuple(1, 2, 3); // → true

Can't be compared with operators like < or >

tuplerone tuples are not supported by the relation comparison operators like <, whereas in a language like Python the following (comparing tuples by arity) would evaluate to true: (1,) < (1, 2).

Array-like but there's no Array prototypes methods

Tuples subclass Array:

Array.isArray(Tuple()); // → true

Yet tuples don't support mutative Array prototype methods like Array.sort(), since tuples are frozen.

The advantage of subclassing Array is ergonomic console representation (it's represented as an array would be), which is based on Array.isArray() and so requires subclassing Array.

Limited number of arities

The tuples are currently typed up to 8-tuple (octuple) because TypeScript doesn't yet support variadic generics. The types are implemented using function overloads.

instanceof doesn't work as expected

Tuples can be constructed without the new keyword to make them behave like other primitive values (Symbol, Boolean, String, Number) that also don't require new and also are value-semantic. This means that instanceof doesn't work the same as for other objects, but can still be used like so:

Tuple() instanceof Tuple.constructor; // → true

License

MIT

Author

slikts dabas@untu.ms

Index

Type aliases

CompositeSymbol

CompositeSymbol<T>: { t: T } & symbol

Type parameters

  • T

CompositeSymbol1

CompositeSymbol1<A>: CompositeSymbol<[A]>

Type parameters

  • A

CompositeSymbol2

CompositeSymbol2<A, B>: CompositeSymbol<[A, B]>

Type parameters

  • A

  • B

CompositeSymbol3

CompositeSymbol3<A, B, C>: CompositeSymbol<[A, B, C]>

Type parameters

  • A

  • B

  • C

CompositeSymbol4

CompositeSymbol4<A, B, C, D>: CompositeSymbol<[A, B, C, D]>

Type parameters

  • A

  • B

  • C

  • D

CompositeSymbol5

CompositeSymbol5<A, B, C, D, E>: CompositeSymbol<[A, B, C, D, E]>

Type parameters

  • A

  • B

  • C

  • D

  • E

CompositeSymbol6

CompositeSymbol6<A, B, C, D, E, F>: CompositeSymbol<[A, B, C, D, E, F]>

Type parameters

  • A

  • B

  • C

  • D

  • E

  • F

CompositeSymbol7

CompositeSymbol7<A, B, C, D, E, F, G>: CompositeSymbol<[A, B, C, D, E, F, G]>

Type parameters

  • A

  • B

  • C

  • D

  • E

  • F

  • G

CompositeSymbol8

CompositeSymbol8<A, B, C, D, E, F, G, H>: CompositeSymbol<[A, B, C, D, E, F, G, H]>

Type parameters

  • A

  • B

  • C

  • D

  • E

  • F

  • G

  • H

GetSettable

GetSettable<A, B>: Settable<A, B> & Gettable<A, B>

Type parameters

  • A

  • B

Primitive

Primitive: boolean | undefined | null | number | string | symbol

Variables

Const CompositeSymbol0

CompositeSymbol0: CompositeSymbol<[never]> = Symbol('CompositeSymbol0') as any

Const UnsafeCompositeSymbol

UnsafeCompositeSymbol: tuple = unsafeSymbol as typeof tuple

Const cache

cache: Map<symbol, object> = new Map<symbol, object>()

Const defaultCache

defaultCache: WeakMap<object, any> = new WeakMap()

Const localToken

localToken: unique symbol = Symbol()

symbol

symbol: symbol

Const symbolKey

symbolKey: unique symbol = Symbol()

tuple

tuple: tuple

Let tuple0

tuple0: Tuple0

Const tupleKey

tupleKey: unique symbol = Symbol()

unsafe

unsafe: unsafe

Const unsafeCache

unsafeCache: Map<any, any> = new Map()

unsafeSymbol

unsafeSymbol: unsafeSymbol

Functions

Const DeepCompositeSymbol

  • DeepCompositeSymbol(object: any, filter?: undefined | ((entry: [string, any]) => boolean)): any
  • Recursively creates a "composite key" (like a "value identity") for an object's entries (key-value pairs).

    Parameters

    • object: any
    • Optional filter: undefined | ((entry: [string, any]) => boolean)

    Returns any

Const ValueObject

  • ValueObject<A>(object: A, filter?: undefined | ((entry: [string, any]) => boolean)): A

Const arraylikeToIterable

  • arraylikeToIterable<A>(source: ArrayLike<A>): IterableIterator<A>
  • Type parameters

    • A

    Parameters

    • source: ArrayLike<A>

    Returns IterableIterator<A>

Const assignArraylike

  • assignArraylike<A>(iterator: Iterator<A>, target: Indexable<A>): number
  • Sets all items from an iterable as index properties on the target object.

    Type parameters

    • A

    Parameters

    Returns number

Const flatten

  • flatten(entries: any[][]): any[]

Const forEach

  • forEach<A>(iterator: Iterator<A>, callback: (value: A) => void): void
  • Type parameters

    • A

    Parameters

    • iterator: Iterator<A>
    • callback: (value: A) => void
        • (value: A): void
        • Parameters

          • value: A

          Returns void

    Returns void

Const getDefault

  • getDefault<A, B>(key: A, defaultValue: B, target: GenericMap<A, B>): B
  • Gets a map element, initializing it with a default value.

    Type parameters

    • A

    • B

    Parameters

    Returns B

Const getDefaultLazy

  • getDefaultLazy<A, B>(key: A, init: () => B, target: GenericMap<A, B>): B
  • Gets a map element, lazily initializing it with a default value.

    Type parameters

    • A

    • B

    Parameters

    • key: A
    • init: () => B
        • (): B
        • Returns B

    • target: GenericMap<A, B>

    Returns B

Const getLeaf

  • getLeaf(values: any[], unsafe?: undefined | false | true): WeakishMap<any, any>
  • Tries to use the first non-primitive from value list as the root key and throws if there's only primitives.

    Parameters

    • values: any[]
    • Optional unsafe: undefined | false | true

    Returns WeakishMap<any, any>

Const getUnsafeLeaf

  • getUnsafeLeaf(values: any[]): Map<any, any>
  • A memory-leaky, slightly more efficient version of getLeaf().

    Parameters

    • values: any[]

    Returns Map<any, any>

Const initUnsafe

  • initUnsafe(): Map<any, any>

Const initWeakish

Const isObject

  • isObject(x: any): x is object
  • Tests if a value is an object.

    Doesn't test for symbols because symbols are invalid as WeakMap keys.

    Parameters

    • x: any

    Returns x is object

Const memoize

  • memoize<A>(fn: A, cache?: WeakMap<object, any>): A
  • Type parameters

    • A: Function

    Parameters

    • fn: A
    • Default value cache: WeakMap<object, any> = defaultCache

    Returns A

Const update

  • update(entry: any, filter?: any): void

Legend

  • Constructor
  • Property
  • Method
  • Inherited constructor
  • Inherited property
  • Inherited method
  • Property
  • Method
  • Private property
  • Static method

Generated using TypeDoc