Collections

Viper C++ provides strongly-typed collections that mirror the JavaScript built-ins while enforcing type safety. The Python equivalent is Collections; the collections are identical — only the idiom differs.

Each collection comes as a TypeX (the parameterized type) and a ValueX (a value of that type). Build a value either with its constructor, new ValueX(type[, init]), or through the universal factory, Value.create(type[, init]). See Types and Values for the constructor rules.

Note

These are Viper values, so they follow Java-like value semantics. Use .equals, .hash, and .compare (each accepts a native argument) rather than the JavaScript operators — == between two values is reference equality, never value equality. INT64/UINT64 elements round-trip through bigint, so integer literals carry the n suffix (42n).

Examples assume the binding is in scope:

const {
    Value, Type,
    ValueVector, TypeVector,
    ValueSet, TypeSet,
    ValueMap, TypeMap,
    ValueTuple, TypeTuple,
    ValueOptional, TypeOptional,
    ValueVariant, TypeVariant,
    ValueXArray, TypeXArray, ValueUUId,
    ValueAny,
} = require('@digitalsubstrate/dsviper');

Vector

ValueVector is the ordered, integer-indexed sequence:

const v = Value.create(new TypeVector(Type.STRING), ['hello', 'world']);
[...v];               // ['hello', 'world']
v.size();             // 2

Append a single element, or extend with a native array:

v.append('!');
v.extend(['from', 'node']);
[...v];   // ['hello', 'world', '!', 'from', 'node']

Index access reads with .at(i) and writes with .set(i, value):

const n = new ValueVector(new TypeVector(Type.INT64), [10n, 20n, 30n]);
n.at(0);          // 10n
n.set(0, 99n);
n.at(0);          // 99n
n.size();         // 3

Vectors are iterable, so spread them into a native array:

[...new ValueVector(new TypeVector(Type.INT64), [1n, 2n, 3n])];   // [1n, 2n, 3n]

.contains, .insert(i, value), .pop([i]), .remove(value), and .clear() round out the list protocol:

const m = new ValueVector(new TypeVector(Type.INT64), [1n, 2n, 3n]);
m.contains(2n);   // true
m.insert(0, 0n);  // [0n, 1n, 2n, 3n]
m.pop();          // 3n  (removes the last)
m.remove(1n);     // removes the first occurrence

Error cases

An out-of-range index or an absent element throws:

const v = new ValueVector(new TypeVector(Type.INT64), [1n, 2n, 3n]);
v.at(10);      // throws — out of range
v.remove(99n); // throws — absent element

Set

ValueSet is a sorted, deduplicated set. Iterating always yields the elements in sorted order, regardless of insertion order:

const T = new TypeSet(Type.INT64);
const s = new ValueSet(T, [3n, 1n, 4n, 1n, 5n]);
[...s];        // [1n, 3n, 4n, 5n]  (sorted, deduplicated)
s.size();      // 4

Add, test membership, and remove:

s.add(2n);
s.contains(2n);   // true
s.remove(1n);     // throws if absent
s.discard(99n);   // silent if absent

Set algebra returns a new set and leaves the operands unchanged. Each operation also accepts a native array (the element type is known, so it is decoded for you):

const a = new ValueSet(T, [1n, 2n, 3n, 4n]);
const b = new ValueSet(T, [3n, 4n, 5n, 6n]);

[...a.union(b)];                 // [1n, 2n, 3n, 4n, 5n, 6n]
[...a.intersection(b)];          // [3n, 4n]
[...a.difference(b)];            // [1n, 2n]
[...a.symmetricDifference(b)];   // [1n, 2n, 5n, 6n]

[...a.union([7n, 8n])];          // native array accepted

The in-place variants mutate the receiver: .update, .intersectionUpdate, .differenceUpdate, .symmetricDifferenceUpdate. Predicates .issubset, .issuperset, and .isdisjoint also accept a set or a native array:

new ValueSet(T, [1n, 2n]).issubset([1n, 2n, 3n]);   // true
new ValueSet(T, [1n, 2n]).isdisjoint([3n, 4n]);     // true

Map

ValueMap is a sorted-key map. A native map literal is an array of [key, value] pairs; spreading yields the entries in sorted-key order (like a JavaScript Map):

const T = new TypeMap(Type.INT64, Type.STRING);
const m = new ValueMap(T, [[2n, 'Two'], [1n, 'One']]);
[...m];   // [[1n, 'One'], [2n, 'Two']]  (keys sorted)

Read with .at(key), write with .set(key, value):

m.set(3n, 'Three');
m.at(1n);         // 'One'
m.contains(3n);   // true
m.size();         // 3

.at(missing) throws; .get(key[, default]) is the safe read:

m.get(99n, 'D');   // 'D'  (default for a missing key)
m.get(99n);        // undefined

Keys, values, and items each come back as a Viper collection — a ValueSet of keys, a ValueVector of values, a ValueVector of [key, value] tuples — all in key order:

[...m.keys()];                        // [1n, 2n, 3n]
[...m.values()];                      // ['One', 'Two', 'Three']
[...m.items()].map((t) => [...t]);    // [[1n, 'One'], [2n, 'Two'], [3n, 'Three']]

Remove with .remove(key) (throws if absent) or .discard(key) (silent); merge with .update(other), accepting a map or native pairs:

m.remove(1n);
m.update([[4n, 'Four']]);   // native pairs accepted

Complex keys

Any value type can be a key — including a composite. Pass a wrapped value as the key:

const mapType = new TypeMap(new TypeMap(Type.STRING, Type.INT64), Type.STRING);
const mm = new ValueMap(mapType);
const k1 = new ValueMap(new TypeMap(Type.STRING, Type.INT64), [['a', 1n]]);
mm.set(k1, 'first');
mm.at(k1);   // 'first'

Optional

ValueOptional holds a value or nothing. A bare construction is nil:

const T = new TypeOptional(Type.INT64);
const opt = new ValueOptional(T);
opt.isNil();   // true

Wrap a value, read it back with .unwrap(), and clear it:

opt.wrap(42n);
opt.isNil();    // false
opt.unwrap();   // 42n
opt.clear();
opt.isNil();    // true

Construct with an initial value directly, or use .get(default) to avoid the empty case:

new ValueOptional(T, 42n).unwrap();   // 42n
new ValueOptional(T).get(99n);        // 99n  (default when nil)

Unwrapping a nil optional throws:

new ValueOptional(T).unwrap();   // throws — the optional is nil

Tuple

ValueTuple holds a fixed, heterogeneous sequence. Construct it from a native array:

const T = new TypeTuple([Type.STRING, Type.INT64, Type.BOOL]);
const t = new ValueTuple(T, ['hello', 42n, true]);
[...t];        // ['hello', 42n, true]
t.size();      // 3

Read with .at(i); mutate in place with .set(i, value) or build a modified copy with .replace(i, value):

t.at(0);                  // 'hello'
t.set(1, 99n);
t.at(1);                  // 99n
const w = t.replace(0, 'bye');   // new tuple; t is unchanged

A wrong-type element throws:

new ValueTuple(T, [42n, 'hello', true]);   // throws — types swapped

Variant

ValueVariant holds one value drawn from a set of member types. The active member is chosen by what you wrap:

const T = new TypeVariant([Type.BOOL, Type.INT64, Type.STRING]);
const v = new ValueVariant(T, 'a string');
v.unwrap();   // 'a string'

.wrap(value) swaps the active value — possibly changing the member type. You can pin the member explicitly with a second type argument:

v.wrap(42n);
v.unwrap();           // 42n
v.wrap(true);
v.unwrap();           // true
v.wrap(42n, Type.INT64);   // explicit member type

A non-member value is rejected:

v.wrap(3.14);   // throws — double is not a member of bool|int64|string

Note

Member identity is exact: true and 1n select different members, so a variant wrapping true is not equal to one wrapping 1n.

XArray

ValueXArray is a sequence addressed by stable UUID positions rather than integer indices, so concurrent inserts and removes neither collide nor shift one another. It preserves insertion order, but its identity is positional: two arrays with the same values but distinct UUIDs are not equal, whereas .copy() (which preserves positions) is equal.

Build it from a native array, then read it back as a plain sequence by iterating or via .toVector():

const T = new TypeXArray(Type.INT64);
const x = new ValueXArray(T, [1n, 2n, 3n]);
[...x];              // [1n, 2n, 3n]
[...x.toVector()];   // [1n, 2n, 3n]  (a ValueVector)

.append(value) and .extend(array) add at the end and return the END marker (the zero/invalid UUID that means “the end of the array”), not the new position. .insert(before, value) inserts before a position and returns the real new position:

x.append(4n);               // returns ValueXArray.END
ValueXArray.END.isValid();  // false

const x2 = new ValueXArray(T, [1n, 2n]);
const p = x2.insert(ValueXArray.END, 3n);   // append; returns the new position
p.isValid();   // true

Recover a position from an integer index with .position(i) (or from a value with .positionOf(value)), then use it with .at, .set, and .remove:

const v = new ValueXArray(T, [10n, 20n, 30n]);
const pos1 = v.position(1);
v.at(pos1);          // 20n
v.set(pos1, 99n);
v.at(pos1);          // 99n
v.index(pos1);       // 1

v.remove(v.position(0));
[...v];              // [99n, 30n]

Note

ValueXArray does not bind .size() — its length is [...x].length. An invalid position passed to .at, .set, or .remove throws.

Any

ValueAny is a type-erased holder for any single value. Construct it empty (nil) or around a value, which it wraps by deducing the type:

const a = new ValueAny();
a.isNil();   // true

a.wrap(42n);
a.isNil();    // false
a.unwrap();   // 42n
a.type().equals(Type.ANY);   // true

It re-wraps freely across types, and holds containers as well as primitives:

const v = new ValueAny(42n);
v.wrap('string');   v.unwrap();   // 'string'
v.wrap(true);       v.unwrap();   // true

const vec = new ValueVector(new TypeVector(Type.INT64), [1n, 2n, 3n]);
v.wrap(vec);
v.unwrap().equals(vec);   // true
[...v.unwrap()];          // [1n, 2n, 3n]

.clear() returns it to nil; unwrapping a nil ValueAny throws:

const a = new ValueAny(42n);
a.clear();
a.isNil();      // true
a.unwrap();     // throws — the holder is nil

Nested collections

Collections compose freely — a value of one collection can be an element, a key, or the wrapped payload of another:

const T = new TypeVector(new TypeMap(Type.STRING, Type.INT64));
const v = new ValueVector(T);
v.append(new ValueMap(new TypeMap(Type.STRING, Type.INT64), [['a', 1n]]));
v.append(new ValueMap(new TypeMap(Type.STRING, Type.INT64), [['b', 2n], ['c', 3n]]));
v.size();   // 2
[...v.at(0)];   // [['a', 1n]]

The same holds for wrappers — an Optional, a Variant, or a ValueAny can hold a container, and .unwrap() returns the wrapped Viper value:

const ot = new TypeOptional(new TypeSet(Type.INT64));
const some = new ValueOptional(ot, new ValueSet(new TypeSet(Type.INT64), [1n, 2n, 3n]));
some.unwrap() instanceof ValueSet;   // true
[...some.unwrap()];                  // [1n, 2n, 3n]