Exploration of TypeScript typed Records

This commit is contained in:
Tony Garnock-Jones 2021-02-22 12:51:17 +01:00
parent a69297c3ba
commit cff1a3d318
7 changed files with 112 additions and 82 deletions

View File

@ -69,9 +69,9 @@ export function strip<T extends object = DefaultPointer>(v: Value<T>, depth: num
function walk(v: Value<T>): Value<T> { return step(v, nextDepth); }
if (Record.isRecord<T>(v.item)) {
return new Record(step(v.item.label, depth), v.item.map(walk));
return Record(step(v.item.label, depth), v.item.map(walk));
} else if (Array.isArray(v.item)) {
return v.item.map(walk);
return (v.item as Array<Value<T>>).map(walk);
} else if (Set.isSet<T>(v.item)) {
return v.item.map(walk);
} else if (Dictionary.isDictionary<Value<T>, T>(v.item)) {

View File

@ -178,7 +178,7 @@ export class Decoder<T extends object> {
case Tag.Record: {
const vs = this.nextvalues();
if (vs.length === 0) throw new DecodeError("Too few elements in encoded record");
return this.wrap(new Record(vs[0], vs.slice(1)));
return this.wrap(Record(vs[0], vs.slice(1)));
}
case Tag.Sequence: return this.wrap(this.nextvalues());
case Tag.Set: return this.wrap(new Set(this.nextvalues()));

View File

@ -11,7 +11,7 @@ export interface FoldMethods<T extends object, R> {
bytes(b: Bytes): R;
symbol(s: symbol): R;
record(r: Record<T>, k: Fold<T, R>): R;
record(r: Record<any, any, T>, k: Fold<T, R>): R;
array(a: Array<Value<T>>, k: Fold<T, R>): R;
set(s: Set<T>, k: Fold<T, R>): R;
dictionary(d: Dictionary<Value<T>, T>, k: Fold<T, R>): R;
@ -43,8 +43,8 @@ export abstract class ValueFold<T extends object, R extends object = T> implemen
symbol(s: symbol): Value<R> {
return s;
}
record(r: Record<T>, k: Fold<T, Value<R>>): Value<R> {
return new Record(k(r.label), r.map(k));
record(r: Record<any, any, T>, k: Fold<T, Value<R>>): Value<R> {
return Record(k(r.label), r.map(k));
}
array(a: Value<T>[], k: Fold<T, Value<R>>): Value<R> {
return a.map(k);

View File

@ -1,14 +1,78 @@
import { Tag } from "./constants";
import { Encoder } from "./codec";
import { PreserveOn } from "./symbols";
import { DefaultPointer, fromJS, is, Value } from "./values";
import { DefaultPointer, is, Value } from "./values";
import { Tuple, TupleMap } from "./tuple";
export const IsPreservesRecord = Symbol.for('IsPreservesRecord');
export class Record<T extends object = DefaultPointer> extends Array<Value<T>> {
export type Record<LabelType extends Value<T>, FieldsType extends Tuple<Value<T>>, T extends object = DefaultPointer>
= { readonly label: LabelType } & FieldsType & RecordImpl<T>;
export interface RecordConstructor<LabelType extends Value<T>, FieldsType extends Tuple<Value<T>>, T extends object = DefaultPointer> {
(...fields: FieldsType): Record<LabelType, FieldsType, T>;
constructorInfo: RecordConstructorInfo<LabelType, T>;
isClassOf(v: any): v is Record<LabelType, FieldsType, T>;
_: { [getter: string]: (r: any) => Value<T> | undefined };
}
export interface RecordConstructorInfo<LabelType extends Value<T>, T extends object = DefaultPointer> {
label: LabelType;
arity: number;
}
export function Record<LabelType extends Value<T>, FieldsType extends Tuple<Value<T>>, T extends object = DefaultPointer>
(label: LabelType, fields: FieldsType)
: Record<LabelType, FieldsType, T>
{
return new RecordImpl(label, fields) as unknown as Record<LabelType, FieldsType, T>;
}
export namespace Record {
export function fallbackToString(_f: Value<any>): string {
return '<unprintable_preserves_field_value>';
}
export function makeConstructor<T extends object = DefaultPointer>(): <LabelType extends Value<T>, FieldNamesType extends Tuple<string>, FieldsType extends TupleMap<FieldNamesType, Value<T>>>(label: LabelType, fieldNames: FieldNamesType) => RecordConstructor<LabelType, FieldsType, T> {
return <LabelType extends Value<T>, FieldNamesType extends Tuple<string>, FieldsType extends TupleMap<FieldNamesType, Value<T>>>(label: LabelType, fieldNames: FieldNamesType) => {
const arity = fieldNames.length;
const ctor: RecordConstructor<LabelType, FieldsType, T> = (...fields: FieldsType): Record<LabelType, FieldsType, T> => {
if (fields.length !== arity) {
throw new Error("Record: cannot instantiate " + (label && (label as any).toString()) +
" expecting " + arity + " fields with " + fields.length + " fields");
}
return Record(label, fields);
};
const constructorInfo = { label, arity };
ctor.constructorInfo = constructorInfo;
ctor.isClassOf = (v: any): v is Record<LabelType, FieldsType, T> => Record.isClassOf(constructorInfo, v);
ctor._ = {};
fieldNames.forEach((name, i) => {
ctor._[name] = function (r: any): Value<T> | undefined {
if (!ctor.isClassOf(r)) {
throw new Error("Record: attempt to retrieve field "+(label as any).toString()+"."+name+
" from non-"+(label as any).toString()+": "+(r && r.toString()));
}
return r[i];
};
});
return ctor;
};
}
export function isRecord<T extends object = DefaultPointer>(x: any): x is Record<Value<T>, Tuple<Value<T>>, T> {
return !!x?.[IsPreservesRecord];
}
export function isClassOf<LabelType extends Value<T>, FieldsType extends Tuple<Value<T>>, T extends object = DefaultPointer>(ci: RecordConstructorInfo<LabelType, T>, v: any): v is Record<LabelType, FieldsType, T> {
return (Record.isRecord(v)) && is(ci.label, v.label) && (ci.arity === v.length);
}
}
export class RecordImpl<T extends object = DefaultPointer> extends Array<Value<T>> {
readonly label: Value<T>;
constructor(label: Value<T>, fieldsJS: any[]) {
constructor(label: Value<T>, fields: Array<Value<T>>) {
if (arguments.length === 1) {
// Using things like someRecord.map() involves the runtime
// apparently instantiating instances of this.constructor
@ -20,8 +84,8 @@ export class Record<T extends object = DefaultPointer> extends Array<Value<T>> {
return;
}
super(fieldsJS.length);
fieldsJS.forEach((f, i) => this[i] = fromJS(f));
super(fields.length);
fields.forEach((f, i) => this[i] = f);
this.label = label;
Object.freeze(this);
}
@ -30,18 +94,18 @@ export class Record<T extends object = DefaultPointer> extends Array<Value<T>> {
return (index < this.length) ? this[index] : defaultValue;
}
set(index: number, newValue: Value<T>): Record<T> {
return new Record(this.label, this.map((f, i) => (i === index) ? newValue : f));
set(index: number, newValue: Value<T>): this {
return <this> new RecordImpl(this.label, this.map((f, i) => (i === index) ? newValue : f));
}
getConstructorInfo(): RecordConstructorInfo<T> {
getConstructorInfo(): RecordConstructorInfo<Value<T>, T> {
return { label: this.label, arity: this.length };
}
equals(other: any): boolean {
return Record.isRecord(other) &&
is(this.label, other.label) &&
this.every((f, i) => is(f, other.get(i)));
this.every((f, i) => is(f, other[i]));
}
// hashCode(): number {
@ -50,8 +114,6 @@ export class Record<T extends object = DefaultPointer> extends Array<Value<T>> {
// return h;
// }
static fallbackToString: (f: Value<any>) => string = (_f) => '<unprintable_preserves_field_value>';
toString(): string {
return this.asPreservesText();
}
@ -71,36 +133,6 @@ export class Record<T extends object = DefaultPointer> extends Array<Value<T>> {
}).join(', ') + ')';
}
static makeConstructor<T extends object = DefaultPointer>(labelSymbolText: string, fieldNames: string[]): RecordConstructor<T> {
return Record.makeBasicConstructor(Symbol.for(labelSymbolText), fieldNames);
}
static makeBasicConstructor<T extends object = DefaultPointer>(label0: any, fieldNames: string[]): RecordConstructor<T> {
const label = fromJS<T>(label0);
const arity = fieldNames.length;
const ctor: RecordConstructor<T> = (...fields: any[]): Record<T> => {
if (fields.length !== arity) {
throw new Error("Record: cannot instantiate " + (label && label.toString()) +
" expecting " + arity + " fields with " + fields.length + " fields");
}
return new Record<T>(label, fields);
};
const constructorInfo = { label, arity };
ctor.constructorInfo = constructorInfo;
ctor.isClassOf = (v: any): v is Record<T> => Record.isClassOf(constructorInfo, v);
ctor._ = {};
fieldNames.forEach((name, i) => {
ctor._[name] = function (r: any): Value<T> | undefined {
if (!ctor.isClassOf(r)) {
throw new Error("Record: attempt to retrieve field "+label.toString()+"."+name+
" from non-"+label.toString()+": "+(r && r.toString()));
}
return r.get(i);
};
});
return ctor;
}
[PreserveOn](encoder: Encoder<T>) {
encoder.emitbyte(Tag.Record);
encoder.push(this.label);
@ -111,24 +143,4 @@ export class Record<T extends object = DefaultPointer> extends Array<Value<T>> {
get [IsPreservesRecord](): boolean {
return true;
}
static isRecord<T extends object = DefaultPointer>(x: any): x is Record<T> {
return !!x?.[IsPreservesRecord];
}
static isClassOf<T extends object = DefaultPointer>(ci: RecordConstructorInfo<T>, v: any): v is Record<T> {
return (Record.isRecord(v)) && is(ci.label, v.label) && (ci.arity === v.length);
}
}
export interface RecordConstructor<T extends object = DefaultPointer> {
(...fields: any[]): Record<T>;
constructorInfo: RecordConstructorInfo<T>;
isClassOf(v: any): v is Record<T>;
_: { [getter: string]: (r: any) => Value<T> | undefined };
}
export interface RecordConstructorInfo<T extends object = DefaultPointer> {
label: Value<T>;
arity: number;
}

View File

@ -0,0 +1,15 @@
// This Tuple type (and tuple() function) is a hack to induce
// TypeScript to infer tuple types rather than array types. (Source:
// https://github.com/microsoft/TypeScript/issues/27179#issuecomment-422606990)
//
// Without it, [123, 'hi', true] will often get the type (string |
// number | boolean)[] instead of [number, string, boolean].
export type Tuple<T> = T[] | [T];
export const tuple = <T, A extends Tuple<T>>(... args: A) => args;
export const Tuple = Array;
export type TupleMap<T extends Tuple<any>, V> = Tuple<V> & {
[K in keyof T]: V;
}

View File

@ -6,18 +6,20 @@ import { DoubleFloat, SingleFloat } from './float';
import { Record } from './record';
import { Annotated } from './annotated';
import { Set, Dictionary } from './dictionary';
import { Tuple } from './tuple';
export * from './bytes';
export * from './float';
export * from './record';
export * from './annotated';
export * from './dictionary';
export * from './tuple';
export type DefaultPointer = object
export type Value<T extends object = DefaultPointer> = Atom | Compound<T> | T | Annotated<T>;
export type Atom = boolean | SingleFloat | DoubleFloat | number | string | Bytes | symbol;
export type Compound<T extends object = DefaultPointer> = Record<T> | Array<Value<T>> | Set<T> | Dictionary<Value<T>, T>;
export type Compound<T extends object = DefaultPointer> = Record<any, Tuple<any>, T> | Array<Value<T>> | Set<T> | Dictionary<Value<T>, T>;
export function fromJS<T extends object = DefaultPointer>(x: any): Value<T> {
switch (typeof x) {

View File

@ -9,6 +9,7 @@ import {
preserves,
fromJS,
Constants,
tuple,
} from '../src/index';
const { Tag } = Constants;
import './test-utils';
@ -35,9 +36,9 @@ function encodePointer(w: Pointer): Value<Pointer> {
return w.v;
}
const Discard = Record.makeConstructor<Pointer>('discard', []);
const Capture = Record.makeConstructor<Pointer>('capture', ['pattern']);
const Observe = Record.makeConstructor<Pointer>('observe', ['pattern']);
const Discard = Record.makeConstructor<Pointer>()(Symbol.for('discard'), []);
const Capture = Record.makeConstructor<Pointer>()(Symbol.for('capture'), ['pattern']);
const Observe = Record.makeConstructor<Pointer>()(Symbol.for('observe'), ['pattern']);
describe('record constructors', () => {
it('should have constructorInfo', () => {
@ -51,8 +52,8 @@ describe('record constructors', () => {
})
describe('RecordConstructorInfo', () => {
const C1 = Record.makeBasicConstructor([1], ['x', 'y']);
const C2 = Record.makeBasicConstructor([1], ['z', 'w']);
const C1 = Record.makeConstructor()(tuple(1), ['x', 'y']);
const C2 = Record.makeConstructor()(tuple(1), ['z', 'w']);
it('instance comparison should ignore pointer and fieldname differences', () => {
expect(C1(9,9)).is(C2(9,9));
expect(C1(9,9)).not.is(C2(9,8));
@ -167,7 +168,7 @@ describe('common test suite', () => {
const samples_bin = fs.readFileSync(__dirname + '/../../../tests/samples.bin');
const samples = decodeWithAnnotations(samples_bin, { decodePointer });
const TestCases = Record.makeConstructor('TestCases', ['cases']);
const TestCases = Record.makeConstructor()(Symbol.for('TestCases'), ['cases']);
function DS(bs: Bytes) {
return decode(bs, { decodePointer });
@ -192,21 +193,21 @@ describe('common test suite', () => {
annotate<Pointer>(2, 1),
annotate<Pointer>(4, 3)),
back: 5 },
annotation5: { forward: annotate(new Record<Pointer>(Symbol.for('R'),
annotation5: { forward: annotate(Record(Symbol.for('R'),
[annotate(Symbol.for('f'),
Symbol.for('af'))]),
Symbol.for('ar')),
back: new Record<Pointer>(Symbol.for('R'), [Symbol.for('f')]) },
annotation6: { forward: new Record<Pointer>(annotate<Pointer>(Symbol.for('R'),
back: Record(Symbol.for('R'), [Symbol.for('f')]) },
annotation6: { forward: Record(annotate<Pointer>(Symbol.for('R'),
Symbol.for('ar')),
[annotate(Symbol.for('f'),
Symbol.for('af'))]),
back: new Record<Pointer>(Symbol.for('R'), [Symbol.for('f')]) },
back: Record(Symbol.for('R'), [Symbol.for('f')]) },
annotation7: { forward: annotate<Pointer>([], Symbol.for('a'), Symbol.for('b'), Symbol.for('c')),
back: [] },
list1: { forward: [1, 2, 3, 4],
back: [1, 2, 3, 4] },
record2: { value: Observe(new Record(Symbol.for("speak"), [
record2: { value: Observe(Record(Symbol.for("speak"), [
Discard(),
Capture(Discard())
])) },
@ -244,7 +245,7 @@ describe('common test suite', () => {
const tests = peel(TestCases._.cases(peel(samples))!) as Dictionary<Value<Pointer>, Pointer>;
tests.forEach((t0: Value<Pointer>, tName0: Value<Pointer>) => {
const tName = Symbol.keyFor(strip(tName0) as symbol)!;
const t = peel(t0) as Record<Pointer>;
const t = peel(t0) as Record<any, any>;
switch (t.label) {
case Symbol.for('Test'):
runTestCase('normal', tName, strip(t[0]) as Bytes, t[1]);