Better collections; package types

This commit is contained in:
Tony Garnock-Jones 2021-01-09 16:21:25 +01:00
parent 55db55b42b
commit b0ed7e914b
7 changed files with 320 additions and 169 deletions

View File

@ -9,6 +9,7 @@
},
"repository": "gitlab:preserves/preserves",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"author": "Tony Garnock-Jones <tonyg@leastfixedpoint.com>",
"devDependencies": {
"@types/jest": "^26.0.19",

View File

@ -4,7 +4,6 @@ import {
underlying,
Annotated,
Dictionary, Set, Bytes, Record, Single, Double,
isSet, isDictionary,
BytesLike,
Value,
} from './values';
@ -117,8 +116,8 @@ export class Decoder {
return this.includeAnnotations ? new Annotated(v) : v;
}
static dictionaryFromArray(vs: Value[]): Dictionary {
const d = new Dictionary();
static dictionaryFromArray(vs: Value[]): Dictionary<Value> {
const d = new Dictionary<Value>();
if (vs.length % 2) throw new DecodeError("Missing dictionary value");
for (let i = 0; i < vs.length; i += 2) {
d.set(vs[i], vs[i+1]);
@ -294,7 +293,7 @@ export class Encoder {
this.emitbyte(Tag.End);
}
push(v: Value) {
push(v: any) {
if (typeof v === 'object' && v !== null && typeof v[PreserveOn] === 'function') {
v[PreserveOn](this);
}
@ -327,30 +326,6 @@ export class Encoder {
else if (Array.isArray(v)) {
this.encodevalues(Tag.Sequence, v);
}
else if (isSet(v)) {
if (this.canonical) {
const pieces = v._map((_v, k) => encode(k, { canonical: true }));
pieces.sort(Bytes.compare);
this.encoderawvalues(Tag.Set, pieces);
} else {
this.encodevalues(Tag.Set, v);
}
}
else if (isDictionary(v)) {
if (this.canonical) {
const pieces = v._map((v, k) => Bytes.concat([encode(k, { canonical: true }),
encode(v, { canonical: true })]));
pieces.sort(Bytes.compare);
this.encoderawvalues(Tag.Dictionary, pieces);
} else {
this.emitbyte(Tag.Dictionary);
v._forEach((v, k) => {
this.push(k);
this.push(v);
});
this.emitbyte(Tag.End);
}
}
else if (typeof v === 'object' && v !== null && typeof v[Symbol.iterator] === 'function') {
this.encodevalues(Tag.Sequence, v as Iterable<Value>);
}
@ -361,10 +336,10 @@ export class Encoder {
}
}
export function encode(v: Value, options?: EncoderOptions): Bytes {
export function encode(v: any, options?: EncoderOptions): Bytes {
return new Encoder(options).push(v).contents();
}
export function encodeWithAnnotations(v: Value, options: EncoderOptions = {}): Bytes {
export function encodeWithAnnotations(v: any, options: EncoderOptions = {}): Bytes {
return encode(v, { ... options, includeAnnotations: true });
}

View File

@ -0,0 +1,223 @@
// FlexMap, FlexSet: like built-in Map and Set, but with a
// canonicalization function which gives us the possibility of a
// coarser equivalence than the identity equivalence used in Map and
// Set.
// A Canonicalizer represents the equivalence you have in mind. For
//
// c: Canonicalizer<V>
// eqv: Equivalence<V>
// v1: V
// v2: V
//
// where `eqv` is the equivalence you want,
//
// eqv(v1, v2) ⇔ c(v1) === c(v2)
//
export type Canonicalizer<V> = (v: V) => string;
export type Equivalence<V> = (v1: V, v2: V) => boolean;
export type IdentityMap<K, V> = Map<K, V>;
export type IdentitySet<V> = Set<V>;
export const IdentityMap = Map;
export const IdentitySet = Set;
export function _iterMap<S,T>(i: Iterator<S> | undefined, f : (s: S) => T): IterableIterator<T> {
if (!i) return void 0;
const _f = (r: IteratorResult<S>): IteratorResult<T> => {
const { done, value } = r;
return { done, value: done ? void 0 : f(value) };
};
return {
next: (v?: any): IteratorResult<T> => _f(i.next(v)),
return: (v?: any): IteratorResult<T> => _f(i.return(v)),
throw: (e?: any): IteratorResult<T> => _f(i.throw(e)),
[Symbol.iterator]() { return this; },
};
}
export class FlexMap<K, V> implements Map<K, V> {
readonly items: Map<string, [K, V]>;
readonly canonicalizer: Canonicalizer<K>;
constructor(c: Canonicalizer<K>, items?: Iterable<readonly [K, V]>) {
this.canonicalizer = c;
this.items = new Map((items === void 0)
? void 0
: _iterMap(items[Symbol.iterator](), ([k, v]) => [this._key(k), [k, v]]));
}
_key(k: K): string {
return this.canonicalizer(k);
}
get(k: K, defaultValue?: V): V | undefined {
const e = this.items.get(this._key(k));
return (e === void 0) ? defaultValue : e[1];
}
set(k: K, v: V): this {
this.items.set(this._key(k), [k, v]);
return this;
}
forEach(f: <T extends Map<K, V>> (v: V, k: K, map: T) => void, thisArg?: any) {
this.items.forEach(([k, v]) => f.call(thisArg, v, k, this));
}
entries(): IterableIterator<[K, V]> {
return this.items.values();
}
keys(): IterableIterator<K> {
return _iterMap(this.items.values(), ([k, _v]) => k);
}
values(): IterableIterator<V> {
return _iterMap(this.items.values(), ([_k, v]) => v);
}
delete(k: K): boolean {
return this.items.delete(this._key(k));
}
clear() {
this.items.clear();
}
has(k: K): boolean {
return this.items.has(this._key(k));
}
get size(): number {
return this.items.size;
}
[Symbol.iterator](): IterableIterator<[K, V]> {
return this.items.values();
}
[Symbol.toStringTag] = 'FlexMap';
equals(other: any, eqv: Equivalence<V> = (v1, v2) => v1 === v2): boolean {
if (!('size' in other && 'has' in other && 'get' in other)) return false;
if (this.size !== other.size) return false;
for (let [k, v] of this.items.values()) {
if (!other.has(k)) return false;
if (!eqv(v, other.get(k))) return false;
}
return true;
}
update(key: K,
f: (oldValue?: V) => V | undefined,
defaultValue?: V,
eqv: Equivalence<V> = (v1, v2) => v1 === v2): number
{
const ks = this._key(key);
if (this.items.has(ks)) {
const oldValue = this.items.get(ks)[1];
const newValue = f(oldValue);
if (newValue === void 0) {
this.items.delete(ks);
return -1;
} else {
if (!eqv(newValue, oldValue)) this.items.set(ks, [key, newValue]);
return 0;
}
} else {
const newValue = f(defaultValue);
if (newValue === void 0) {
return 0;
} else {
this.items.set(ks, [key, newValue]);
return 1;
}
}
}
canonicalKeys(): IterableIterator<string> {
return this.items.keys();
}
}
export class FlexSet<V> implements Set<V> {
readonly items: Map<string, V>;
readonly canonicalizer: Canonicalizer<V>;
constructor(c: Canonicalizer<V>, items?: Iterable<V>) {
this.canonicalizer = c;
this.items = new Map((items === void 0)
? void 0
: _iterMap(items[Symbol.iterator](), (v) => [this._key(v), v]));
}
_key(v: V): string {
return this.canonicalizer(v);
}
has(v: V): boolean {
return this.items.has(this._key(v));
}
get(v: V): {item: V} | null {
const vs = this._key(v);
if (this.items.has(vs)) {
return {item: this.items[vs]};
} else {
return null;
}
}
add(v: V): this {
this.items[this._key(v)] = v;
return this;
}
forEach(f: <T extends Set<V>>(v: V, v2: V, set: T) => void, thisArg?: any) {
this.items.forEach((v) => f.call(thisArg, v, v, this));
}
entries(): IterableIterator<[V, V]> {
return _iterMap(this.items.values(), (v) => [v, v]);
}
keys(): IterableIterator<V> {
return this.items.values();
}
values(): IterableIterator<V> {
return this.items.values();
}
delete(v: V): boolean {
return this.items.delete(this._key(v));
}
clear() {
this.items.clear();
}
get size(): number {
return this.items.size;
}
[Symbol.iterator](): IterableIterator<V> {
return this.items.values();
}
[Symbol.toStringTag] = 'FlexSet';
equals(other: any): boolean {
if (!('size' in other && 'has' in other)) return false;
if (this.size !== other.size) return false;
for (let v of this.items.values()) {
if (!other.has(v)) return false;
}
return true;
}
canonicalValues(): IterableIterator<string> {
return this.items.keys();
}
}

View File

@ -1,3 +1,4 @@
export * from './flex';
export * from './symbols';
export * from './codec';
export * from './values';

View File

@ -1,10 +1,14 @@
import { Value } from './values';
export function stringify(x: Value): string {
export function stringify(x: any): string {
if (typeof x === 'object' && x !== null && 'asPreservesText' in x) {
return x.asPreservesText();
} else {
return JSON.stringify(x);
try {
return JSON.stringify(x);
} catch (_e) {
return ('' + x).asPreservesText();
}
}
}

View File

@ -3,13 +3,15 @@
import { PreserveOn, AsPreserve } from './symbols';
import { Tag } from './constants';
import { Encoder, encode } from './codec';
import { stringify } from './text';
import { _iterMap, FlexMap, FlexSet, IdentityMap, IdentitySet } from './flex';
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
export type Value = Atom | Compound | Annotated;
export type Atom = boolean | Single | Double | number | string | Bytes | symbol;
export type Compound = Record | Array<Value> | Set | Dictionary;
export type Compound = Record | Array<Value> | Set | Dictionary<Value>;
export function isRecord(x: any): x is Record {
return Array.isArray(x) && 'label' in x;
@ -517,12 +519,14 @@ export interface RecordConstructorInfo {
arity: number;
}
export function is(a: Value, b: Value): boolean {
export function is(a: any, b: any): boolean {
if (isAnnotated(a)) a = a.item;
if (isAnnotated(b)) b = b.item;
if (Object.is(a, b)) return true;
if (typeof a !== typeof b) return false;
if (typeof a === 'object') {
if (a === null || b === null) return false;
if ('equals' in a) return a.equals(b);
if ('equals' in a) return a.equals(b, is);
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) if (!is(a[i], b[i])) return false;
@ -542,176 +546,119 @@ export function isClassOf(ci: RecordConstructorInfo, v: any): v is Record {
export type DictionaryType = 'Dictionary' | 'Set';
export function is_Dictionary(x: any, t: DictionaryType): x is _Dictionary {
return typeof x === 'object' && x !== null &&
'_items' in x &&
'_dictionaryType' in x &&
x._dictionaryType() === t;
export function is_Dictionary(x: any, t: DictionaryType): boolean {
return typeof x === 'object' && x !== null && x[Symbol.toStringTag] === t;
}
export const isDictionary = (x: any): x is Dictionary => is_Dictionary(x, 'Dictionary');
export const isDictionary = <T> (x: any): x is Dictionary<T> => is_Dictionary(x, 'Dictionary');
export const isSet = (x: any): x is Set => is_Dictionary(x, 'Set');
export type DictionaryEntry = [Value, Value];
export function _canonicalString(item: Value): string {
const bs = encode(item, { canonical: true })._view;
const s = String.fromCharCode.apply(null, bs);
return s;
}
export abstract class _Dictionary {
_items: { [key: string]: DictionaryEntry } = {};
_key(key: Value): string {
const bs = encode(key, { canonical: true })._view;
const s = String.fromCharCode.apply(null, bs);
return s;
}
_lookup(key: Value): DictionaryEntry | null {
const k = this._key(key);
return k in this._items ? this._items[k] : null;
}
_set(key: Value, value: Value) {
this._items[this._key(key)] = [key, value];
}
_get(key: Value, defaultValue?: Value): Value {
const k = this._key(key);
return k in this._items ? this._items[k][1] : defaultValue;
}
_delete(key: Value) {
delete this._items[this._key(key)];
}
_forEach(f: (value: Value, key: Value) => void) {
for (let ks in this._items) {
const [k, v] = this._items[ks];
f(v, k);
export class Dictionary<T> extends FlexMap<Value, T> {
static fromJS(x: object): Dictionary<Value> {
if (isDictionary(x)) return x as Dictionary<Value>;
const d = new Dictionary<Value>();
for (let key in x) {
const value = x[key];
d.set(key, fromJS(value));
}
return d;
}
_map<T>(f: (value: Value, key: Value) => T): Array<T> {
const result = [];
for (let ks in this._items) {
const [k, v] = this._items[ks];
result.push(f(v, k));
constructor(items?: Iterable<readonly [any, T]>) {
super(_canonicalString, _iterMap(items?.[Symbol.iterator](), ([k,v]) => [fromJS(k), v]));
}
mapEntries<R>(f: (entry: [Value, T]) => [Value, R]): Dictionary<R> {
const result = new Dictionary<R>();
for (let oldEntry of this.entries()) {
const newEntry = f(oldEntry);
result.set(newEntry[0], newEntry[1])
}
return result;
}
equals(other: any): boolean {
if (!Object.is(other.constructor, this.constructor)) return false;
const es1 = Object.entries(this._items);
if (es1.length !== Object.entries(other._items).length) return false;
for (let [ks1, e1] of es1) {
const e2 = other._items[ks1];
if (!is(e1[1], e2[1])) return false;
}
return true;
asPreservesText(): string {
return '{' +
Array.from(_iterMap(this.entries(), ([k, v]) =>
k.asPreservesText() + ': ' + stringify(v))).join(', ') +
'}';
}
clone(): Dictionary<T> {
return new Dictionary(this);
}
toString(): string {
return this.asPreservesText();
}
abstract asPreservesText(): string;
abstract _dictionaryType(): DictionaryType;
}
[Symbol.toStringTag] = 'Dictionary';
export class Dictionary extends _Dictionary {
static fromJS(x: object): Dictionary {
if (isDictionary(x)) return x;
const d = new Dictionary();
for (let key in x) {
const value = x[key];
d._set(key, fromJS(value));
[PreserveOn](encoder: Encoder) {
if (encoder.canonical) {
const pieces = Array.from(this).map(([k, v]) =>
Bytes.concat([encode(k, { canonical: true }),
encode(v, { canonical: true })]));
pieces.sort(Bytes.compare);
encoder.encoderawvalues(Tag.Dictionary, pieces);
} else {
encoder.emitbyte(Tag.Dictionary);
this.forEach((v, k) => {
encoder.push(k);
encoder.push(v);
});
encoder.emitbyte(Tag.End);
}
return d;
}
_dictionaryType(): DictionaryType {
return 'Dictionary';
}
set(key: Value, value: Value) {
this._set(key, value);
}
get(key: Value, defaultValue?: Value): Value {
return this._get(key, defaultValue);
}
delete(key: Value) {
this._delete(key);
}
mapEntries(f: (entry: DictionaryEntry) => DictionaryEntry): Dictionary {
const result = new Dictionary();
for (let ks in this._items) {
const oldEntry = this._items[ks];
const newEntry = f(oldEntry);
result._set(newEntry[0], newEntry[1])
}
return result;
}
forEach(f: (value: Value, key: Value) => void) {
this._forEach(f);
}
asPreservesText(): string {
return '{' +
this._map((v, k) => k.asPreservesText() + ': ' + v.asPreservesText()).join(', ') +
'}';
}
}
export class Set extends _Dictionary implements Iterable<Value> {
constructor(items: Iterable<any> = []) {
super();
for (let item of items) this.add(fromJS(item));
}
_dictionaryType(): DictionaryType {
return 'Set';
}
add(v: Value) {
this._set(v, true);
}
delete(v: Value) {
this._delete(v);
}
includes(key: Value) {
return this._lookup(key) !== null;
}
forEach(f: (value: Value) => void) {
this._forEach((_v, k) => f(k));
export class Set extends FlexSet<Value> {
constructor(items?: Iterable<any>) {
super(_canonicalString, _iterMap(items?.[Symbol.iterator](), fromJS));
}
map(f: (value: Value) => Value): Set {
const result = new Set();
for (let ks in this._items) {
const k = this._items[ks][0];
result._set(f(k), true);
}
return result;
return new Set(_iterMap(this[Symbol.iterator](), f));
}
[Symbol.iterator](): Iterator<Value> {
return this._map((_v, k) => k)[Symbol.iterator]();
filter(f: (value: Value) => boolean): Set {
const result = new Set();
for (let k of this) if (f(k)) result.add(k);
return result;
}
asPreservesText(): string {
return '#{' +
this._map((_v, k) => k.asPreservesText()).join(', ') +
Array.from(_iterMap(this.values(), v => v.asPreservesText())).join(', ') +
'}';
}
clone(): Set {
return new Set(this);
}
[Symbol.toStringTag] = 'Set';
[PreserveOn](encoder: Encoder) {
if (encoder.canonical) {
const pieces = Array.from(this).map(k => encode(k, { canonical: true }));
pieces.sort(Bytes.compare);
encoder.encoderawvalues(Tag.Set, pieces);
} else {
encoder.encodevalues(Tag.Set, this);
}
}
}
export function isAnnotated(x: any): x is Annotated {
return typeof x === 'object' && x !== null &&
x.constructor.name === 'Annotated' &&
'annotations' in x &&
'item' in x;
}
@ -740,7 +687,7 @@ export class Annotated {
}
equals(other: any): boolean {
return isAnnotated(other) && is(this.item, other.item);
return is(this.item, isAnnotated(other) ? other.item : other);
}
hashCode(): number {

View File

@ -132,7 +132,7 @@ describe('common test suite', () => {
});
}
const tests = peel(TestCases._.cases(peel(samples))) as Dictionary;
const tests = peel(TestCases._.cases(peel(samples))) as Dictionary<Value>;
tests.forEach((t0: Value, tName0: Value) => {
const tName = Symbol.keyFor(strip(tName0) as symbol);
const t = peel(t0) as Record;