From 3ed89df6d7b0d00cd7bde1c724a0d1a66fd3229e Mon Sep 17 00:00:00 2001 From: Michael Haufe Date: Sun, 28 May 2023 22:11:11 -0500 Subject: [PATCH] Completed constructor unification --- CHANGELOG.md | 8 ++ README.md | 110 ++++++++++++++-------------- package-lock.json | 4 +- package.json | 4 +- src/BoxedMultiKeyMap.mjs | 18 +++-- src/Pattern.mjs | 14 ++-- src/callable.mjs | 19 ++--- src/complect.mjs | 101 ++++++++++++++----------- src/data.mjs | 94 ++++++++++++------------ src/index.mjs | 2 +- src/normalizeArgs.mjs | 8 +- src/tests/BoxedMultiKeyMap.test.mjs | 78 +++++++++++++++++--- src/tests/Shape.test.mjs | 6 +- src/tests/Tree.test.mjs | 4 +- src/tests/Xml.test.mjs | 6 +- src/tests/arith.test.mjs | 48 +++++++----- src/tests/bool.test.mjs | 5 +- src/tests/color.test.mjs | 32 +++----- src/tests/derived-prop.test.mjs | 10 ++- src/tests/destructuring.test.mjs | 39 +--------- src/tests/extensibility.test.mjs | 64 ++++++++-------- src/tests/fib.test.mjs | 2 +- src/tests/getter.test.mjs | 13 ++-- src/tests/immutability.test.mjs | 48 ++++++------ src/tests/list.test.mjs | 11 ++- src/tests/memoFix.test.mjs | 18 ++--- src/tests/pattern-matching.test.mjs | 14 ++-- src/tests/peano.test.mjs | 32 ++++++-- src/tests/point.test.mjs | 6 +- src/tests/strictEquality.test.mjs | 58 ++++++--------- src/trait.mjs | 9 +-- 31 files changed, 474 insertions(+), 411 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c54856..0871699 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## v0.13.0 + +- Bugfix of memoization edge-cases +- Fixed issue of callable features not being inheritable +- `data`, `trait`, and `complect` now consistently return callable constructors instead of objects/constructors ([#64](https://github.com/mlhaufe/brevity/issues/64)) +- `Complected` is no longer exported and has been eliminated +- Updated README + ## v0.12.0 - Trait is not longer callable and must be complected before use. diff --git a/README.md b/README.md index b968fd6..ae7d11d 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ For the impatient, here is a quick example of how to use Brevity: ```js // declare a data family -const pointData = data({ +const PointData = data({ Point2: { x: Number, y: Number }, Point3: { x: Number, y: Number, z: Number } }); @@ -61,11 +61,11 @@ const Scaleable = trait('scale', { }) // complect the data and operations together -const point = complect(pointData, [Printable, Scaleable]) +const Point = complect(pointData, [Printable, Scaleable]) // use the complected family: -const { Point2, Point3 } = point +const { Point2, Point3 } = Point() const p2 = Point2({ x: 3, y: 2 }), p3 = Point3({ x: 12, y: 37, z: 54 }) @@ -185,7 +185,7 @@ Here is how the above would be approached with Brevity: ```js // data declaration -const expData = data({ +const ExpData = data({ Lit: { value: {} }, Add: { left: {}, right: {} } }) @@ -209,8 +209,8 @@ const PrintTrait = trait('print', { Usage: ```js -const exp = complect(expData, [EvalTrait, PrintTrait]), - {Add, Lit} = exp +const Exp = complect(expData, [EvalTrait, PrintTrait]), + {Add, Lit} = Exp() // 1 + 3 const add = Add(Lit(1), Lit(3)) @@ -223,19 +223,20 @@ add.print() // "1 + 3" Adding a new data type `Mul` is as simple as extending the base data type: ```js -const mulExpData = data(exp, { +const MulExpData = data(Exp, { Mul: { left: {}, right: {} } }) ``` -To extend `evaluate` and `print` simply extend the existing traits: +To extend `evaluate` and `print` simply extend the existing traits or the +complected object: ```js -const EvalMulTrait = trait(exp, 'evaluate', { +const EvalMulTrait = trait(Exp, 'evaluate', { Mul({left,right}){ return left.evaluate() * right.evaluate() } }) -const PrintMulTrait = trait(exp, 'print', { +const PrintMulTrait = trait(Exp, 'print', { Mul({left,right}){ return `${left.print()} * ${right.print()}` } }) ``` @@ -253,8 +254,8 @@ const IsValueTrait = trait('isValue', { Then at some point complect the data and traits together: ```js -const mulExp = complect(mulExpData, [EvalMulTrait, PrintMulTrait, IsValueTrait]), - {Add, Lit, Mul} = mulExp +const MulExp = complect(MulExpData, [EvalMulTrait, PrintMulTrait, IsValueTrait]), + {Add, Lit, Mul} = MulExp() ``` ## Data @@ -264,12 +265,14 @@ const mulExp = complect(mulExpData, [EvalMulTrait, PrintMulTrait, IsValueTrait]) Enumerations can be declared similar to how you would in a functional language: ```js -const colorData = data({ Red: {}, Green: {}, Blue: {} }); +const ColorData = data({ Red: {}, Green: {}, Blue: {} }); ``` Variants without properties are considered singletons: ```js +const {Red, Green, Blue} = ColorData() + const red = colorData.Red, red2 = colorData.Red @@ -279,11 +282,11 @@ red === red2 Each variant can have properties. These properties become named parameters of each constructor: ```js -const pointData = data({ +const PointData = data({ Point2: {x: {}, y: {}}, Point3: {x: {}, y: {}, z: {}} }), - {Point2, Point3} = pointData + {Point2, Point3} = PointData() const p2 = Point2({x: 3, y: 2}), p3 = Point3({x: 12, y: 37, z: 54}) @@ -308,11 +311,11 @@ Data declarations support guards: ```js // Constructor guards -const pointData = data({ +const PointData = data({ Point2: { x: Number, y: Number }, Point3: { x: Number, y: Number, z: Number } }), -{ Point2, Point3 } = pointData; +{ Point2, Point3 } = PointData(); // TypeError: Guard mismatch on property 'y'. Expected: Number, got: "2" Point2(1, '2') @@ -322,11 +325,11 @@ Recursive forms are also supported: ```js // Recursive guard: -const peanoData = data((Peano) => ({ +const PeanoData = data(() => ({ Zero: {}, - Succ: { pred: Peano } + Succ: { pred: PeanoData } })), -{ Zero, Succ } = peanoData; +{ Zero, Succ } = PeanoData(); const z = Zero, one = Succ(z), @@ -336,14 +339,13 @@ const z = Zero, Succ(1) ``` -The parameter is always the self reference. Since there are no addiional parameters, the declaratation `peanoData` remains -an object. If there were additional parameters, the declaration would become a function: +If there are additional parameters: ```js // Parameterized recursive guard: -const ListData = data((List, T) => ({ +const ListData = data((T) => ({ Nil: {}, - Cons: { head: T, tail: List(T) } + Cons: { head: T, tail: ListData(T) } })); const numListData = ListData(Number), @@ -412,7 +414,7 @@ const IntBoolExp = data(IntExp, { Bool: { value: {} }, Iff: { pred: {}, ifTrue: {}, ifFalse: {}} }), - {Add, Lit, Bool, Iff} = IntBoolExp + {Add, Lit, Bool, Iff} = IntBoolExp() // if (true) 1 else 1 + 3 const exp = Iff( Bool(true), Lit(1), Add(Lit(1), Lit(3)) ) @@ -425,7 +427,7 @@ const exp = Iff( Bool(true), Lit(1), Add(Lit(1), Lit(3)) ) ```js const {Employee} = data({ Employee: {firstName: String, lastName: String, fullName: String} -}) +})() const p = Employee({ firstName: 'John', @@ -446,7 +448,7 @@ const Lang = data({ Char: {value: {}}, Empty: {}, }), - { Alt, Empty, Cat, Char } = Lang + { Alt, Empty, Cat, Char } = Lang() // balanced parentheses grammar // S = S ( S ) ∪ ε @@ -474,7 +476,7 @@ Non-singleton variants also support strict equality comparisons: const {Point2, Point3} = data({ Point2: {x: Number, y: Number}, Point3: {x: Number, y: Number, z: Number} -}) +})() Point3(1,2,3) === Point3({x:1, y:2, z:3}) // true ``` @@ -488,7 +490,7 @@ directly like Array, Map, Set, etc. const { Point2, Point3 } = data({ Point2: {x: Number, y: Number}, Point3: {x: Number, y: Number, z: Number} -}) +})() const pArray = [Point2(1, 2), Point3(1, 2, 3)]; @@ -526,7 +528,7 @@ pMap.has(Point3(1, 2, 4)) // false A `trait` defines operations for data family and supports pattern matching. ```js -const colorData = data({ Red: {}, Green: {}, Blue: {} }); +const ColorData = data({ Red: {}, Green: {}, Blue: {} }); const Printable = trait('print', { Red() { return '#FF0000' }, @@ -535,9 +537,11 @@ const Printable = trait('print', { }) // 'complect' combines data and traits. It will be explained later. -const color = complect(colorData, [Printable]) +const Color = complect(colorData, [Printable]) + +const { Red, Green, Blue } = Color() -color.Red.print() // '#FF0000' +Red.print() // '#FF0000' ``` Another example on a recursive structure: @@ -598,11 +602,11 @@ More advanced pattern matching is supported beyond simply variants and utilize ` as a wildcard. This is accomplished via the `Pattern` constructor: ```js -const expData = data({ - Num: {value: {}}, - Var: {name: {}}, - Mul: {left: {}, right: {}} - }) +const ExpData = data({ + Num: {value: {}}, + Var: {name: {}}, + Mul: {left: {}, right: {}} +}) // 1 * x = x // x * 1 = x @@ -620,7 +624,7 @@ const SimplifyTrait = trait('simplify', { ]) }) -const { Num, Var, Mul } = complect(expData, [SimplifyTrait]) +const { Num, Var, Mul } = complect(expData, [SimplifyTrait])() const e1 = Mul(Var('x'), Num(1)) @@ -656,7 +660,7 @@ const SimplifyTrait = trait('simplify', { A more complicated example with nested patterns: ```js -const list = data({ Nil: {}, Cons: {head: {}, tail: {}} }) +const List = data({ Nil: {}, Cons: {head: {}, tail: {}} }) const TellTrait = trait('tell', { Nil: (self) => 'The list is empty', @@ -757,7 +761,7 @@ define traits that won't become stuck in infinite recursion when those fields ar Given the following contrived trait you can see that it will blow the stack when called: ```js -const numData = data({ +const NumData = data({ Num: { n: Number } }) @@ -765,7 +769,7 @@ const OmegaTrait = trait('omega', { Num({ n }) { return this.Num(n).omega(); } }) -const { Num } = complect(numData, [OmegaTrait]) +const { Num } = complect(NumData, [OmegaTrait]) Num(2).omega() // new Error('Maximum call stack size exceeded') ``` @@ -778,7 +782,7 @@ const OmegaFixTrait = trait('omegaFix', { Num({ n }) { return this.Num(n).omegaFix(); } }) -const { Num } = complect(numData, [OmegaFixTrait]) +const { Num } = complect(NumData, [OmegaFixTrait]) Num(2).omegaFix() // 'bottom' ``` @@ -786,7 +790,7 @@ Num(2).omegaFix() // 'bottom' A `bottom` value can also be a function which will be called with the respective arguments to determine what the bottom value should be: ```js -const fooData = data({ +const FooData = data({ Foo: { n: Number } }) @@ -800,7 +804,7 @@ const FooFixTrait = trait('foo', { } }) -const { Foo } = complect(fooData, [FooFixTrait]) +const { Foo } = complect(FooData, [FooFixTrait]) FooFix(1).foo() === 19; FooFix(2).foo() === 18; @@ -814,7 +818,7 @@ initial entry in this cache. So the added benefit of this is not just for tying- for improving performance: ```js -const fibData = data({ +const FibData = data({ Fib: { n: Number } }) @@ -831,7 +835,7 @@ const FixEvalTrait = trait('fixEval', { } }) -const { Fib } = complect(fibData, [Evaluable, FixEvalTrait]) +const { Fib } = complect(FibData, [Evaluable, FixEvalTrait]) let start, end; @@ -841,7 +845,7 @@ end = performance.now(); const time = end - start; // ~4333ms start = performance.now(); -Fib(30).fixEval(); +Fib(40).fixEval(); end = performance.now(); const memoTime = end - start; // ~0.1ms ``` @@ -851,7 +855,7 @@ const memoTime = end - start; // ~0.1ms Data and associated traits can be combined into a single object via the `complect` function: ```js -const pointData = data({ +const PointData = data({ Point2: { x: Number, y: Number }, Point3: { x: Number, y: Number, z: Number } }) @@ -861,7 +865,7 @@ const Printable = trait('print', { Point3({ x, y, z }) { return `(${x}, ${y}, ${z})` } }) -const { Point2, Point3 } = complect(pointData, [printable]) +const { Point2, Point3 } = complect(PointData, [printable]) ``` `complect` is a function that takes a data declaration and an object of traits. It returns an object with the data declaration's constructors as keys and the traits applied to them as values. @@ -880,9 +884,9 @@ p2.print() // '(1, 2, 3)' If the data declaration is parameterized, like ListData, then complect will return a function that takes the parameters and returns the complected object: ```js -const ListData = data((List, T) => ({ +const ListData = data((T) => ({ Nil: {}, - Cons: { head: T, tail: List(T) } + Cons: { head: T, tail: ListData(T) } })); const ConcatTrait = trait('concat', { @@ -911,7 +915,7 @@ const List = complect(ListData, [ConcatTrait, IsNilTrait, LengthTrait]), A data declaration can extend a complected object: ```js -const point4Data = data(pointData, { +const Point4Data = data(PointData, { Point4: { x: Number, y: Number, z: Number, w: Number } }) ``` @@ -919,7 +923,7 @@ const point4Data = data(pointData, { A trait declaration can also extend a complected object: ```js -const point4Printable = trait(point, { +const Point4Printable = trait(Point, { Point4({ x, y, z, w }) { return `(${x}, ${y}, ${z}, ${w})` } }) ``` diff --git a/package-lock.json b/package-lock.json index 090c6d0..c619a1d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@mlhaufe/brevity", - "version": "0.12.0", + "version": "0.13.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@mlhaufe/brevity", - "version": "0.12.0", + "version": "0.13.0", "license": "AGPL-3.0-only", "devDependencies": { "@types/jest": "^29.2.3", diff --git a/package.json b/package.json index bcc4acb..e53a312 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@mlhaufe/brevity", - "version": "0.12.0", + "version": "0.13.0", "description": "A library for Feature-Oriented Programming", "keywords": [ "Object Algebra", @@ -45,4 +45,4 @@ "webpack": "^5.83.1", "webpack-cli": "^5.0.1" } -} +} \ No newline at end of file diff --git a/src/BoxedMultiKeyMap.mjs b/src/BoxedMultiKeyMap.mjs index a186bee..512f2c1 100644 --- a/src/BoxedMultiKeyMap.mjs +++ b/src/BoxedMultiKeyMap.mjs @@ -48,6 +48,8 @@ export class BoxedMultiKeyMap { * map.get(1, 2, 3); // 4 */ get(...keys) { + if (keys.length < 1) + throw new TypeError('Must provide at least one key'); const lastMap = keys.reduce((map, key) => { if (map === undefined) return undefined; @@ -57,8 +59,9 @@ export class BoxedMultiKeyMap { if (lastMap === undefined) return undefined; + const result = lastMap.get(this.#boxKey(keys[keys.length - 1])); - return lastMap.get(this.#boxKey(keys[keys.length - 1])); + return result instanceof WeakMap ? undefined : result; } /** @@ -72,6 +75,8 @@ export class BoxedMultiKeyMap { * map.get(1, 2, 3); // 4 */ set(...keysAndValue) { + if (keysAndValue.length < 2) + throw new TypeError('Must provide at least one key and a value'); const keys = keysAndValue.slice(0, -1), value = keysAndValue[keysAndValue.length - 1]; @@ -101,18 +106,17 @@ export class BoxedMultiKeyMap { * map.get(1); // undefined */ delete(...keys) { + if (keys.length < 1) + throw new TypeError('Must provide at least one key'); + if (!this.has(...keys)) + throw new TypeError('No value exists for the given keys'); const lastMap = keys.reduce((map, key) => { if (map === undefined) return undefined; const objKey = this.#boxKey(key); - if (!map.has(objKey)) - return undefined; return map.get(objKey); }, this.#map); - if (lastMap === undefined) - return; - lastMap.delete(this.#boxKey(keys[keys.length - 1])); } @@ -128,6 +132,8 @@ export class BoxedMultiKeyMap { * map.has(1, 2, 3, 4); // false */ has(...keys) { + if (keys.length < 1) + throw new TypeError('Must provide at least one key'); const lastMap = keys.reduce((map, key) => { if (map === undefined) return undefined; diff --git a/src/Pattern.mjs b/src/Pattern.mjs index 0a5b65e..f0814a9 100644 --- a/src/Pattern.mjs +++ b/src/Pattern.mjs @@ -1,8 +1,7 @@ import { callable } from "./callable.mjs"; -import { Complected } from "./complect.mjs"; -import { Data } from "./data.mjs"; +import { Data, BaseVariant } from "./data.mjs"; import { isConstructor, isObjectLiteral, isPrimitive } from "./predicates.mjs"; -import { _ } from "./symbols.mjs"; +import { _, dataDecl } from "./symbols.mjs"; /** * A Primitive value @@ -32,7 +31,7 @@ import { _ } from "./symbols.mjs"; const isPattern = (p) => { return isPrimitive(p) || isObjectLiteral(p) || Array.isArray(p) || isConstructor(p) || p === _ - || p instanceof Data || p instanceof Complected + || p instanceof Data[BaseVariant] || p[dataDecl] } const satisfiesPrimitive = (Cons, value) => { @@ -51,11 +50,8 @@ const unify = (p, a) => { return true } else if (isPrimitive(p)) { return p === a - } else if (p instanceof Data || p instanceof Complected) { - if (!(a instanceof Data) && !(a instanceof Complected)) - return false - // TODO: need a dataDecl comparison and not just a name comparison - if (p.constructor.name !== a.constructor.name) + } else if (p instanceof Data[BaseVariant]) { + if (!(a instanceof p.constructor)) return false for (const [k, v] of Object.entries(p)) { if (!(k in a)) diff --git a/src/callable.mjs b/src/callable.mjs index d237dd0..147fa56 100644 --- a/src/callable.mjs +++ b/src/callable.mjs @@ -5,6 +5,12 @@ // @returns {(...args: Parameters>) => T} - The callable class +const _callHandler = { + apply(Target, _thisArg, argArray) { + return Reflect.construct(Target, argArray,) + } +} + /** * Make a class callable * @template T @@ -12,16 +18,5 @@ * @returns - The callable class */ export function callable(Clazz) { - function Create(...args) { - return new Clazz(...args); - } - Object.setPrototypeOf(Create, Clazz); - Object.setPrototypeOf(Create.prototype, Clazz.prototype); - Object.defineProperties(Create, { - name: { value: Clazz.name }, - length: { value: Clazz.length }, - [Symbol.hasInstance]: { value(instance) { return instance instanceof Clazz } } - }) - - return Create; + return new Proxy(Clazz, _callHandler) } \ No newline at end of file diff --git a/src/complect.mjs b/src/complect.mjs index 2bb2809..d4a96c9 100644 --- a/src/complect.mjs +++ b/src/complect.mjs @@ -3,69 +3,84 @@ import { apply, Trait } from "./trait.mjs"; import { dataDecl, dataVariant, traitDecls } from "./symbols.mjs"; import { Data } from "./data.mjs"; import { callable } from "./callable.mjs"; +import { isConstructor } from "./predicates.mjs"; +import { normalizeArgs } from "./normalizeArgs.mjs"; -export class Complected { - *[Symbol.iterator]() { for (let k in this) yield this[k] } -} +/** + * @template T + * @typedef {import("./symbols.mjs").Constructor} Constructor + */ /** * Complects a data declaration with traits - * @param {*} dataDef - * @param {*} traits + * @param {Constructor} DataCons + * @param {(Constructor)[]} Traits * @returns {*} */ -export function complect(dataDef, traits) { - if (!(dataDef instanceof Data) && typeof dataDef !== 'function') - throw new TypeError('Expected a data declaration or a function') - if (!Array.isArray(traits) || !traits.every(t => t.prototype instanceof Trait)) +export function complect(DataCons, Traits = []) { + if (!isConstructor(DataCons) && DataCons.prototype instanceof Data) + throw new TypeError('Invalid dataDef. A Data declaration was expected'); + if (!Array.isArray(Traits) || !Traits.every(T => isConstructor(T) && T.prototype instanceof Trait)) throw new TypeError('Array of traits expected'); - if (dataDef.length > 0) - return (...args) => complect(dataDef(...args), traits) - class _Complected extends Complected { - get [dataDecl]() { return dataDef } - get [traitDecls]() { return traits } + const memo = new BoxedMultiKeyMap(); + return Object.freeze(callable(class _Complected extends DataCons { + get [dataDecl]() { return DataCons } + get [traitDecls]() { return Traits }; + + constructor(...args) { + super(...args) + if (memo.has(this.constructor, ...args)) + return memo.get(this.constructor, ...args) + memo.set(this.constructor, ...args, this) + const family = this + const _traitProxyHandler = { + get(target, prop) { + // search Traits for the matching method name + for (let TraitCons of Traits) { + const methodName = TraitCons.name + if (prop === methodName) { + const trait = new TraitCons(family) + function method(...args) { return trait[apply](this, ...args) } + Object.defineProperty(method, 'name', { value: methodName }) + return method + } + } + return Reflect.get(target, prop) + } + } - static { - const family = this.prototype - for (let consName in dataDef) { - const DataCons = dataDef[consName], - memo = new BoxedMultiKeyMap() - const ConsComplected = callable(class extends _Complected { + for (let consName in family) { + const Cons = family[consName], + VariantCons = /** @type {Constructor} */ (typeof Cons === 'function' ? Cons : Cons.constructor) + const ConsComplected = callable(class extends VariantCons { static { Object.defineProperties(this, { name: { value: consName }, - length: { value: typeof DataCons === 'function' ? DataCons.length : 0 } + length: { value: VariantCons.length } }) - const proto = this.prototype - for (let TraitCons of traits) { - const methodName = TraitCons.name - if (proto[methodName]) - throw new TypeError(`Invalid traitDecl. Duplicate method name: '${methodName}'`); - const t = new TraitCons(family) - Object.defineProperty(proto, methodName, { - enumerable: false, - value(...args) { return t[apply](this, ...args) } - }) - } } + constructor(...args) { - super() - const dataInstance = typeof DataCons === 'function' ? DataCons(...args) : DataCons, - cached = memo.get(dataInstance); + super(...args) + const propNames = Object.keys(this), + vName = this.constructor.name, + // TODO: associate with super[normalizedArgs] for caching? + normalizedArgs = normalizeArgs(propNames, args, vName), + cached = memo.get(this.constructor, ...normalizedArgs) if (cached) return cached; - Object.assign(this, dataInstance, { [dataVariant]: dataInstance }) - Object.freeze(this) - memo.set(dataInstance, this) + const proxy = new Proxy(this, _traitProxyHandler) + memo.set(this.constructor, ...normalizedArgs, proxy) + return proxy } + + get [dataVariant]() { return VariantCons } }) family[consName] = ConsComplected.length === 0 ? ConsComplected() : ConsComplected } - Object.freeze(family) - } - } - - return Object.freeze(new _Complected()) + //Object.freeze(family) + }; + })) } \ No newline at end of file diff --git a/src/data.mjs b/src/data.mjs index a10c08c..6958204 100644 --- a/src/data.mjs +++ b/src/data.mjs @@ -3,25 +3,27 @@ import { isCamelCase, isCapitalized, isConstructor, isObjectLiteral, isPrimitive, satisfiesPrimitive } from "./predicates.mjs"; -import { Complected } from "./complect.mjs"; import { _, dataDecl, dataVariant } from "./symbols.mjs"; import { normalizeArgs } from "./normalizeArgs.mjs"; import { callable } from "./callable.mjs"; export const BaseVariant = Symbol('BaseVariant') +const typeArgs = Symbol('typeArgs'), + init = Symbol('init') + export class Data { - static [BaseVariant] = class { - *[Symbol.iterator]() { for (let k in this) yield this[k] } + static [BaseVariant] = class IterableVariant { + *[Symbol.iterator]() { + for (let k in this) { + const v = this[k] + if (typeof v !== 'function') + yield this[k] + } + } } } -const TypeRecursion = callable(class { - constructor(...args) { - this.args = args - } -}) - function assignProps(obj, propNames, args) { Object.defineProperties(obj, propNames.reduce((acc, propName, i) => { @@ -51,18 +53,18 @@ function guardCheck(Factory, args, props) { // TODO: { guard: {}, get(){} } return } else if (isConstructor(guard)) { - if (guard === TypeRecursion) { // singleton types + if (guard === Data) { // singleton types const isVariant = value instanceof Data[BaseVariant], - isComplected = value instanceof Complected + isComplected = value[dataDecl] if (!isVariant && !isComplected) - throw new TypeError(errMsg('TypeRecursion', JSON.stringify(value))) + throw new TypeError(errMsg('Data', JSON.stringify(value))) if (isVariant && !(value instanceof Factory[BaseVariant])) - throw new TypeError(errMsg('TypeRecursion', JSON.stringify(value))) + throw new TypeError(errMsg('Data', JSON.stringify(value))) if (isComplected && !(value[dataVariant] instanceof Factory[BaseVariant])) - throw new TypeError(errMsg('TypeRecursion', JSON.stringify(value))) - } else if (guard instanceof TypeRecursion) { // parameterized types + throw new TypeError(errMsg('Data', JSON.stringify(value))) + } else if (guard instanceof Data) { // parameterized types if (!(value instanceof Factory[BaseVariant])) - throw new TypeError(errMsg('TypeRecursion', JSON.stringify(value))) + throw new TypeError(errMsg('Data', JSON.stringify(value))) // TODO: utilize args } else if (isPrimitive(value)) { if (!satisfiesPrimitive(value, guard)) @@ -77,7 +79,7 @@ function guardCheck(Factory, args, props) { if (typeof guard === 'function' && !(value instanceof guard)) throw new TypeError(errMsg(guard.name, JSON.stringify(value))) - if (guard instanceof Data && !(value instanceof Factory)) + if (guard instanceof Data && !(value instanceof Factory[BaseVariant])) throw new TypeError(errMsg(guard, JSON.stringify(value))) } }) @@ -103,36 +105,17 @@ export function data(BaseData, dataDec) { BaseData = Data } - if (BaseData instanceof Complected) + if (BaseData[dataDecl]) BaseData = BaseData[dataDecl] - else if (BaseData.prototype instanceof Complected) - BaseData = BaseData.prototype[dataDecl] - - // TODO: can the function form be moved into the Factory constructor? - let dataDef - if (typeof dataDec === 'function') { - if (dataDec.length > 1) { - const dataCons = (...typeParams) => { - return data(dataDec(TypeRecursion, ...typeParams)) - } - Object.defineProperty(dataCons, 'length', { value: dataDec.length - 1 }) - return dataCons - } else { - dataDef = dataDec(TypeRecursion) - } - } else { - dataDef = dataDec - } - if (!isObjectLiteral(dataDef)) - throw new TypeError('Invalid data declaration. Object literal expected') if (isConstructor(BaseData) && !(BaseData.prototype instanceof Data) && BaseData !== Data) throw new TypeError('Invalid Base reference. A Data declaration was expected') - const Factory = callable(class extends BaseData { - static [BaseVariant] = class extends BaseData[BaseVariant] { } + const memo = new BoxedMultiKeyMap(); + const Factory = callable(class Factory extends BaseData { + static [BaseVariant] = class VariantBase extends BaseData[BaseVariant] { }; - static { + [init](dataDef) { for (let [vName, props] of Object.entries(dataDef)) { if (!isCapitalized(vName)) throw new TypeError(`variant name must be capitalized: ${vName}`); @@ -146,9 +129,9 @@ export function data(BaseData, dataDec) { return true }), memo = new BoxedMultiKeyMap(), - Self = this + Self = this.constructor - const Variant = callable(class extends this[BaseVariant] { + const Variant = callable(class extends Self[BaseVariant] { static { Object.defineProperties(this, { name: { value: vName }, @@ -181,20 +164,35 @@ export function data(BaseData, dataDec) { } constructor(...args) { super() - const normalizedArgs = normalizeArgs(propNames, args, vName), - cached = memo.get(...normalizedArgs); - if (cached) - return cached; + const normalizedArgs = normalizeArgs(propNames, args, vName) + if (memo.has(this.constructor, ...normalizedArgs)) + return memo.get(this.constructor, ...normalizedArgs); guardCheck(Self, normalizedArgs, props) assignProps(this, propNames, normalizedArgs) Object.freeze(this) - memo.set(...[...normalizedArgs, this]) + memo.set(this.constructor, ...normalizedArgs, this) } }) this[vName] = propNames.length === 0 ? Variant() : Variant } } + + [typeArgs] = [] + + constructor(...args) { + super(...args) + if (memo.has(this.constructor, ...args)) + return memo.get(this.constructor, ...args) + memo.set(this.constructor, ...args, this) + this[typeArgs] = args + if (isObjectLiteral(dataDec)) + this[init](dataDec) + else if (typeof dataDec === 'function') + this[init](dataDec(...args)) + else + throw new TypeError('Invalid data declaration. Expected an object literal or a function') + } }) return Object.freeze(Factory) diff --git a/src/index.mjs b/src/index.mjs index 829363f..58a1e52 100644 --- a/src/index.mjs +++ b/src/index.mjs @@ -2,4 +2,4 @@ export { dataDecl, traitDecls, _ } from './symbols.mjs'; export { Data, data } from './data.mjs'; export { Pattern } from './Pattern.mjs'; export { apply, trait, Trait, memoFix } from './trait.mjs'; -export { complect, Complected } from './complect.mjs'; \ No newline at end of file +export { complect } from './complect.mjs'; \ No newline at end of file diff --git a/src/normalizeArgs.mjs b/src/normalizeArgs.mjs index ea88b55..d9af91e 100644 --- a/src/normalizeArgs.mjs +++ b/src/normalizeArgs.mjs @@ -4,10 +4,10 @@ import { isObjectLiteral } from "./predicates.mjs"; * Normalizes the arguments passed to a variant constructor to an array of values * @param {string[]} propNames * @param {any[]} args - * @param {string} VName - The name of the variant + * @param {string} vName - The name of the variant * @returns {any[]} - The normalized arguments */ -export function normalizeArgs(propNames, args, VName) { +export function normalizeArgs(propNames, args, vName) { if (propNames.length === 1) { if (args.length === 1) { const arg = args[0]; @@ -24,7 +24,7 @@ export function normalizeArgs(propNames, args, VName) { if (Object.keys(objArg).length !== propNames.length) throw new TypeError( - `Wrong number of arguments. expected: ${VName}(${propNames}) got: ${VName}(${Object.keys(objArg)})` + `Wrong number of arguments. expected: ${vName}(${propNames}) got: ${vName}(${Object.keys(objArg)})` ); return propNames.map((propName) => { if (!(propName in objArg)) @@ -34,7 +34,7 @@ export function normalizeArgs(propNames, args, VName) { } else if (args.length === propNames.length) { return [...args] } else { - throw new TypeError(`Wrong number of arguments. expected: ${VName}(${propNames}), got: ${VName}(${args})`); + throw new TypeError(`Wrong number of arguments. expected: ${vName}(${propNames}), got: ${vName}(${args})`); } } } \ No newline at end of file diff --git a/src/tests/BoxedMultiKeyMap.test.mjs b/src/tests/BoxedMultiKeyMap.test.mjs index 7f9f08a..5341dad 100644 --- a/src/tests/BoxedMultiKeyMap.test.mjs +++ b/src/tests/BoxedMultiKeyMap.test.mjs @@ -1,15 +1,32 @@ import { BoxedMultiKeyMap } from "../BoxedMultiKeyMap.mjs"; describe("BoxedMultiKeyMap", () => { + test('Empty keys', () => { + const map = new BoxedMultiKeyMap(); + expect(() => map.get()).toThrow(TypeError); + expect(() => map.set()).toThrow(TypeError); + expect(() => map.delete()).toThrow(TypeError); + expect(() => map.has()).toThrow(TypeError); + expect(() => map.set(...[], 1)).toThrow(TypeError); + }) + test('String keys', () => { const map = new BoxedMultiKeyMap(); map.set('a', 'b', 'c', 'd'); expect(map.get('a', 'b', 'c')).toBe('d'); + expect(map.get('a', 'b', 'd')).toBe(undefined); + expect(map.get('a', 'b', 'c', 'd')).toBe(undefined); expect(map.get('a', 'b')).toBe(undefined); expect(map.get('a')).toBe(undefined); - expect(map.get()).toBe(undefined); - expect(map.has('a', 'b', 'c')).toBe(true); + + expect(() => map.delete('a', 'b', 'c', 'd')).toThrow(TypeError); + expect(() => map.delete('a', 'b')).toThrow(TypeError); + expect(() => map.delete('a')).toThrow(TypeError); + expect(map.get('a', 'b', 'c')).toBe('d'); // nothing deleted + + expect(() => map.delete('a', 'b', 'c')).not.toThrow(TypeError); + expect(map.get('a', 'b', 'c')).toBe(undefined); }) test('Number keys', () => { @@ -18,9 +35,36 @@ describe("BoxedMultiKeyMap", () => { expect(map.get(1, 2, 3)).toBe(4); expect(map.get(1, 2)).toBe(undefined); expect(map.get(1)).toBe(undefined); - expect(map.get()).toBe(undefined); - expect(map.has(1, 2, 3)).toBe(true); + + map.set(1, 2, 3, 4, 5); + expect(map.get(1, 2, 3)).toBe(4); + expect(map.get(1, 2, 3, 4)).toBe(5); + expect(map.get(1, 2, 3, 4, 5)).toBe(undefined); + expect(map.has(1, 2, 3, 4)).toBe(true); + expect(map.has(1, 2, 3, 4, 5)).toBe(false); + }) + + test('BigInt keys', () => { + const map = new BoxedMultiKeyMap(); + map.set(1n, 2n, 3n, 4n); + expect(map.get(1n, 2n, 3n)).toBe(4n); + expect(map.get(1n, 2n)).toBe(undefined); + expect(map.get(1n)).toBe(undefined); + expect(map.has(1n, 2n, 3n)).toBe(true); + }) + + test('Lambda keys', () => { + const map = new BoxedMultiKeyMap(); + const a = () => 'a'; + const b = () => 'b'; + const c = () => 'c'; + const d = () => 'd'; + map.set(a, b, c, d); + expect(map.get(a, b, c)).toBe(d); + expect(map.get(a, b)).toBe(undefined); + expect(map.get(a)).toBe(undefined); + expect(map.has(a, b, c)).toBe(true); }) test('Boolean keys', () => { @@ -29,8 +73,6 @@ describe("BoxedMultiKeyMap", () => { expect(map.get(true, false, true)).toBe(false); expect(map.get(true, false)).toBe(undefined); expect(map.get(true)).toBe(undefined); - expect(map.get()).toBe(undefined); - expect(map.has(true, false, true)).toBe(true); }) @@ -44,8 +86,6 @@ describe("BoxedMultiKeyMap", () => { expect(map.get(a, b, c)).toBe(d); expect(map.get(a, b)).toBe(undefined); expect(map.get(a)).toBe(undefined); - expect(map.get()).toBe(undefined); - expect(map.has(a, b, c)).toBe(true); }) @@ -59,8 +99,6 @@ describe("BoxedMultiKeyMap", () => { expect(map.get(a, b, c)).toBe(d); expect(map.get(a, b)).toBe(undefined); expect(map.get(a)).toBe(undefined); - expect(map.get()).toBe(undefined); - expect(map.has(a, b, c)).toBe(true); }); @@ -74,8 +112,24 @@ describe("BoxedMultiKeyMap", () => { expect(map.get(a, b, c)).toBe(d); expect(map.get(a, b)).toBe(undefined); expect(map.get(a)).toBe(undefined); - expect(map.get()).toBe(undefined); - expect(map.has(a, b, c)).toBe(true); }) + + test('null keys', () => { + const map = new BoxedMultiKeyMap(); + map.set(null, null, null, 1); + expect(map.get(null, null, null)).toBe(1); + expect(map.get(null, null)).toBe(undefined); + expect(map.get(null)).toBe(undefined); + expect(map.has(null, null, null)).toBe(true); + }) + + test('undefined keys', () => { + const map = new BoxedMultiKeyMap(); + map.set(undefined, undefined, undefined, 1); + expect(map.get(undefined, undefined, undefined)).toBe(1); + expect(map.get(undefined, undefined)).toBe(undefined); + expect(map.get(undefined)).toBe(undefined); + expect(map.has(undefined, undefined, undefined)).toBe(true); + }) }) \ No newline at end of file diff --git a/src/tests/Shape.test.mjs b/src/tests/Shape.test.mjs index 4ad2efd..455080a 100644 --- a/src/tests/Shape.test.mjs +++ b/src/tests/Shape.test.mjs @@ -1,7 +1,7 @@ import { complect, data, trait } from "../index.mjs"; describe('Shape tests', () => { - const shapeData = data({ + const ShapeData = data({ Circle: { radius: Number }, Rectangle: { width: Number, height: Number } }) @@ -11,8 +11,8 @@ describe('Shape tests', () => { Rectangle({ width, height }) { return width * height } }) - const shape = complect(shapeData, [Areable]), - { Circle, Rectangle } = shape; + const Shape = complect(ShapeData, [Areable]), + { Circle, Rectangle } = Shape(); test('Shape data', () => { const circle = Circle({ radius: 1 }); diff --git a/src/tests/Tree.test.mjs b/src/tests/Tree.test.mjs index f01c4fd..43e93d7 100644 --- a/src/tests/Tree.test.mjs +++ b/src/tests/Tree.test.mjs @@ -1,9 +1,9 @@ import { complect, data, trait, _ } from "../index.mjs" describe('Tree Tests', () => { - const TreeData = data((Tree, T) => ({ + const TreeData = data((T) => ({ Leaf: { value: T }, - Branch: { left: Tree(T), right: Tree(T) } + Branch: { left: TreeData(T), right: TreeData(T) } })); const Printable = trait('print', { diff --git a/src/tests/Xml.test.mjs b/src/tests/Xml.test.mjs index 43842f8..95b1515 100644 --- a/src/tests/Xml.test.mjs +++ b/src/tests/Xml.test.mjs @@ -1,7 +1,7 @@ import { data, trait, complect } from "../index.mjs" describe('Simplified Xml Tests', () => { - const xmlData = data({ + const XmlData = data({ Attr: { name: String, value: String }, Element: { name: String, attrs: {}, children: {} }, Text: { text: String } @@ -22,8 +22,8 @@ describe('Simplified Xml Tests', () => { _: ({ name }) => name }) - const xml = complect(xmlData, [Printable, NodeNameTrait]), - { Attr, Element, Text } = xml; + const Xml = complect(XmlData, [Printable, NodeNameTrait]), + { Attr, Element, Text } = Xml(); // // diff --git a/src/tests/arith.test.mjs b/src/tests/arith.test.mjs index 840be66..c56589f 100644 --- a/src/tests/arith.test.mjs +++ b/src/tests/arith.test.mjs @@ -16,7 +16,7 @@ describe('Arithmetic', () => { }) test('named parameters', () => { - const { Lit, Add } = ExpData + const { Lit, Add } = ExpData() expect(Lit).toBeDefined() expect(Add).toBeDefined() @@ -36,17 +36,17 @@ describe('Arithmetic', () => { }) test('extra parameters throw', () => { - const { Lit, Add } = ExpData + const { Lit, Add } = ExpData() expect(() => Add({ left: Lit(1), right: Lit(2), extra: 3 })).toThrow() }) test('missing parameters throw', () => { - const { Lit, Add } = ExpData + const { Lit, Add } = ExpData() expect(() => Add({ left: Lit(1) })).toThrow() }) test('positional parameters', () => { - const { Lit, Add } = ExpData + const { Lit, Add } = ExpData() // 1 + (2 + 3) const e = Add( Lit(1), @@ -59,16 +59,26 @@ describe('Arithmetic', () => { }) test('wrong number of parameters throw', () => { - const { Lit, Add } = ExpData + const { Lit, Add } = ExpData() expect(() => Add(Lit(1))).toThrow() expect(() => Add(Lit(1), Lit(2), Lit(3))).toThrow() }) test('complect', () => { - const exp = complect(ExpData, [EvalTrait]) - const { Lit, Add } = exp + const Exp = complect(ExpData, [EvalTrait]) + const { Lit, Add } = Exp() - expect(Lit(4).evaluate()).toBe(4) + const lit = Lit(4) + + expect(lit.evaluate).toBeDefined() + expect(lit.evaluate()).toBe(4) + + const lit1 = Lit(1), + lit2 = Lit(2), + add = Add(lit1, lit2) + + expect(add.evaluate).toBeDefined() + expect(add.evaluate()).toBe(3) // 1 + (2 + 3) const e = Add({ @@ -101,8 +111,8 @@ describe('Arithmetic', () => { }) test('print', () => { - const exp = complect(ExpData, [PrintTrait]) - const { Lit, Add } = exp + const Exp = complect(ExpData, [PrintTrait]) + const { Lit, Add } = Exp() expect(Lit(4).print()).toBe('4') expect(Add(Lit(1), Lit(2)).print()).toBe('1 + 2') @@ -138,8 +148,8 @@ describe('Arithmetic', () => { }) test('evalPrint', () => { - const exp = complect(ExpData, [EvaluablePrintable, EvalTrait, PrintTrait]) - const { Lit, Add } = exp + const Exp = complect(ExpData, [EvaluablePrintable, EvalTrait, PrintTrait]) + const { Lit, Add } = Exp() // 1 + (2 + 3) const e = Add(Lit(1), Add(Lit(2), Lit(3))) @@ -152,7 +162,7 @@ describe('Arithmetic', () => { }) test('MulExp data', () => { - const { Lit, Add, Mul } = MulExpData + const { Lit, Add, Mul } = MulExpData() expect(Lit).toBeDefined() expect(Add).toBeDefined() @@ -171,8 +181,8 @@ describe('Arithmetic', () => { }) test('evalMul', () => { - const exp = complect(MulExpData, [EvaluableMul]), - { Lit, Add, Mul } = exp + const Exp = complect(MulExpData, [EvaluableMul]), + { Lit, Add, Mul } = Exp() // 1 + (2 * 3) const e = Add(Lit(1), Mul(Lit(2), Lit(3))) @@ -185,8 +195,8 @@ describe('Arithmetic', () => { }) test('printMul', () => { - const exp = complect(MulExpData, [MulPrintable]), - { Lit, Add, Mul } = exp + const Exp = complect(MulExpData, [MulPrintable]), + { Lit, Add, Mul } = Exp() // 1 + (2 * 3) const e = Add(Lit(1), Mul(Lit(2), Lit(3))) @@ -201,8 +211,8 @@ describe('Arithmetic', () => { }) test('isValue', () => { - const exp = complect(MulExpData, [IsValuable]), - { Lit, Add, Mul } = exp + const Exp = complect(MulExpData, [IsValuable]), + { Lit, Add, Mul } = Exp() // 1 + (2 * 3) const e = Add(Lit(1), Mul(Lit(2), Lit(3))) diff --git a/src/tests/bool.test.mjs b/src/tests/bool.test.mjs index 32fda7b..56d1054 100644 --- a/src/tests/bool.test.mjs +++ b/src/tests/bool.test.mjs @@ -1,7 +1,7 @@ import { complect, data, trait } from "../index.mjs" describe('Bool tests', () => { - const boolData = data({ False: {}, True: {} }) + const BoolData = data({ False: {}, True: {} }) test('Bool traits', () => { const Andable = trait('and', { @@ -19,7 +19,8 @@ describe('Bool tests', () => { True() { return this.False } }) - const { False: f, True: t } = complect(boolData, [Andable, Orable, Notable]) + const Bool = complect(BoolData, [Andable, Orable, Notable]), + { False: f, True: t } = Bool() expect(f.and(f)).toBe(f) expect(f.and(t)).toBe(f) diff --git a/src/tests/color.test.mjs b/src/tests/color.test.mjs index fca935e..445b79b 100644 --- a/src/tests/color.test.mjs +++ b/src/tests/color.test.mjs @@ -2,23 +2,7 @@ import { BaseVariant } from "../data.mjs"; import { complect, data, trait } from "../index.mjs" describe('Color tests', () => { - const rgbData = data({ Red: {}, Green: {}, Blue: {} }); - - test('rgbData', () => { - expect(rgbData).toBeDefined(); - expect(rgbData.Red).toBeDefined(); - expect(rgbData.Green).toBeDefined(); - expect(rgbData.Blue).toBeDefined(); - - const { Red, Green, Blue } = rgbData; - - expect(Red).toBeInstanceOf(rgbData[BaseVariant]); - expect(Green).toBeInstanceOf(rgbData[BaseVariant]); - expect(Blue).toBeInstanceOf(rgbData[BaseVariant]); - expect(Red).not.toBe(Green); - expect(Red).not.toBe(Blue); - expect(Green).not.toBe(Blue); - }) + const RgbData = data({ Red: {}, Green: {}, Blue: {} }); const RgbPrintable = trait('print', { Red() { return '#FF0000' }, @@ -27,18 +11,22 @@ describe('Color tests', () => { }) test('rgbColor', () => { - const color = complect(rgbData, [RgbPrintable]), - { Red, Green, Blue } = color; + const Color = complect(RgbData, [RgbPrintable]), + { Red, Green, Blue } = Color(); expect(Red).toBeDefined(); expect(Green).toBeDefined(); expect(Blue).toBeDefined(); + expect(Red).toBeInstanceOf(RgbData[BaseVariant]) + expect(Green).toBeInstanceOf(RgbData[BaseVariant]) + expect(Blue).toBeInstanceOf(RgbData[BaseVariant]) + expect(Red.print()).toBe('#FF0000'); expect(Green.print()).toBe('#00FF00'); expect(Blue.print()).toBe('#0000FF'); }) - const rgbCmykColor = data(rgbData, { + const RgbCmykColor = data(RgbData, { Cyan: {}, Magenta: {}, Yellow: {}, @@ -53,8 +41,8 @@ describe('Color tests', () => { }) test('CmykColor', () => { - const color = complect(rgbCmykColor, [RgbCmykPrintable]), - { Cyan, Magenta, Yellow, Black, Red, Green, Blue } = color; + const Color = complect(RgbCmykColor, [RgbCmykPrintable]), + { Cyan, Magenta, Yellow, Black, Red, Green, Blue } = Color(); expect(Cyan).toBeDefined(); expect(Magenta).toBeDefined(); diff --git a/src/tests/derived-prop.test.mjs b/src/tests/derived-prop.test.mjs index 3aec9c4..d1cead9 100644 --- a/src/tests/derived-prop.test.mjs +++ b/src/tests/derived-prop.test.mjs @@ -1,8 +1,8 @@ -import { data } from "../index.mjs" +import { complect, data } from "../index.mjs" describe('Derived field tests', () => { test('Employee with derived field', () => { - const { Employee } = data({ + const EmployeeData = data({ Employee: { firstName: String, lastName: String, @@ -13,6 +13,8 @@ describe('Derived field tests', () => { } }) + const { Employee } = complect(EmployeeData)() + const johnDoe = Employee({ firstName: 'John', lastName: 'Doe' @@ -30,7 +32,7 @@ describe('Derived field tests', () => { }) test('Bad derived field', () => { - const { Employee } = data({ + const EmployeeData = data({ Employee: { firstName: String, lastName: String, @@ -41,6 +43,8 @@ describe('Derived field tests', () => { } }) + const { Employee } = complect(EmployeeData)() + const johnDoe = Employee({ firstName: 'John', lastName: 'Doe' diff --git a/src/tests/destructuring.test.mjs b/src/tests/destructuring.test.mjs index 3b11c49..795c492 100644 --- a/src/tests/destructuring.test.mjs +++ b/src/tests/destructuring.test.mjs @@ -1,48 +1,13 @@ import { complect, data, trait } from "../index.mjs" describe('Destructuring', () => { - test(`Point data destructuring`, () => { + test(`Point destructuring`, () => { const PointData = data({ Point2: { x: {}, y: {} }, Point3: { x: {}, y: {}, z: {} } - }), - { Point2, Point3 } = PointData; - - const p2 = Point2({ x: 1, y: 2 }), - p3 = Point3({ x: 1, y: 2, z: 3 }); - - const { x: x2, y: y2 } = p2; - expect(x2).toBe(1); - expect(y2).toBe(2); - - const { x: x3, y: y3, z: z3 } = p3; - expect(x3).toBe(1); - expect(y3).toBe(2); - expect(z3).toBe(3); - - // array destructuring - const [x2a, y2a] = p2; - expect(x2a).toBe(1); - expect(y2a).toBe(2); - - const [x3a, y3a, z3a] = p3; - expect(x3a).toBe(1); - expect(y3a).toBe(2); - expect(z3a).toBe(3); - }) - - test('Complect destructuring', () => { - const PointData = data({ - Point2: { x: {}, y: {} }, - Point3: { x: {}, y: {}, z: {} } - }) - - const Printable = trait('print', { - Point2({ x, y }) { return `Point2(${x}, ${y})` }, - Point3({ x, y, z }) { return `Point3(${x}, ${y}, ${z})` } }) - const { Point2, Point3 } = complect(PointData, [Printable]) + const { Point2, Point3 } = complect(PointData)(); const p2 = Point2({ x: 1, y: 2 }), p3 = Point3({ x: 1, y: 2, z: 3 }); diff --git a/src/tests/extensibility.test.mjs b/src/tests/extensibility.test.mjs index 43ea97f..85a3ed4 100644 --- a/src/tests/extensibility.test.mjs +++ b/src/tests/extensibility.test.mjs @@ -1,7 +1,7 @@ import { apply, data, trait, complect, dataDecl } from "../index.mjs" describe('Extensibility for the Masses', () => { - const intExpData = data({ + const IntExpData = data({ Lit: { value: {} }, Add: { left: {}, right: {} } }) @@ -23,7 +23,7 @@ describe('Extensibility for the Masses', () => { }) test('IntExp', () => { - const { Lit, Add } = complect(intExpData, [IntPrintable, IntEvaluable]) + const { Lit, Add } = complect(IntExpData, [IntPrintable, IntEvaluable])() const e = Add({ left: Lit(1), @@ -34,13 +34,15 @@ describe('Extensibility for the Masses', () => { expect(e.evaluate()).toBe(6) }) - const intBoolExpData = data(intExpData, { + const IntBoolExpData = data(IntExpData, { Bool: { value: {} }, IIf: { pred: {}, ifTrue: {}, ifFalse: {} } }) - test('IntBoolExp data', () => { - const { Lit, Add, Bool, IIf } = intBoolExpData + test('IntBoolExp', () => { + const IntBoolExp = complect(IntBoolExpData, [IntPrintable, IntEvaluable]) + + const { Lit, Add, Bool, IIf } = IntBoolExp() expect(Lit).toBeDefined() expect(Add).toBeDefined() @@ -63,7 +65,7 @@ describe('Extensibility for the Masses', () => { }); test('IntBoolPrintable', () => { - const { Bool, IIf, Lit } = complect(intBoolExpData, [IntBoolPrintable]) + const { Bool, IIf, Lit } = complect(IntBoolExpData, [IntBoolPrintable])() const e = IIf(Bool(true), Lit(1), Lit(2)) @@ -78,14 +80,14 @@ describe('Extensibility for the Masses', () => { }) test('intBoolEval', () => { - const { Bool, IIf, Lit } = complect(intBoolExpData, [IntBoolEvaluable]) + const { Bool, IIf, Lit } = complect(IntBoolExpData, [IntBoolEvaluable])() const e = IIf(Bool(true), Lit(1), Lit(2)) expect(e.evaluate()).toBe(1) }) - const stmtExpData = data(intBoolExpData, { + const StmtExpData = data(IntBoolExpData, { Assign: { scope: {}, name: {}, value: {} }, Expr: { value: {} }, Seq: { first: {}, second: {} }, @@ -93,7 +95,7 @@ describe('Extensibility for the Masses', () => { }) test('StmtExp data', () => { - const { Lit, Add, Bool, IIf, Assign, Expr, Seq, Var } = stmtExpData + const { Lit, Add, Bool, IIf, Assign, Expr, Seq, Var } = complect(StmtExpData)() expect(Lit).toBeDefined() expect(Add).toBeDefined() @@ -132,8 +134,8 @@ describe('Extensibility for the Masses', () => { }) test('stmtPrint', () => { - const exp = complect(stmtExpData, [StmtPrintable]), - { Lit, Add, Bool, IIf, Assign, Expr, Seq, Var } = exp + const Exp = complect(StmtExpData, [StmtPrintable])(), + { Lit, Add, Bool, IIf, Assign, Expr, Seq, Var } = Exp const scope = new Map() scope.set('x', 1) @@ -180,8 +182,8 @@ describe('Extensibility for the Masses', () => { }) test('stmtEval', () => { - const exp = complect(stmtExpData, [StmtEvaluable]), - { Lit, Add, Bool, IIf, Assign, Expr, Seq, Var } = exp + const Exp = complect(StmtExpData, [StmtEvaluable])(), + { Lit, Add, Bool, IIf, Assign, Expr, Seq, Var } = Exp const scope = new Map() scope.set('x', 1) @@ -247,44 +249,46 @@ describe('Extensibility for the Masses', () => { }) test('data extend complected', () => { - const intExp = complect(intExpData, [IntPrintable, IntEvaluable]) + const IntExp = complect(IntExpData, [IntPrintable, IntEvaluable]) - const intBoolExpData = data(intExp, { + const IntBoolExpData = data(IntExp, { Bool: { value: {} }, IIf: { pred: {}, ifTrue: {}, ifFalse: {} } }) - expect(intBoolExpData.Lit).toBeDefined() - expect(intBoolExpData.Add).toBeDefined() - expect(intBoolExpData.Bool).toBeDefined() - expect(intBoolExpData.IIf).toBeDefined() + const { Lit, Add, Bool, IIf } = complect(IntBoolExpData)() + + expect(Lit).toBeDefined() + expect(Add).toBeDefined() + expect(Bool).toBeDefined() + expect(IIf).toBeDefined() }) test('trait extend complected', () => { - const intExp = complect(intExpData, [IntPrintable, IntEvaluable]) + const IntExp = complect(IntExpData, [IntPrintable, IntEvaluable]) - const IntBoolExpData = data(intExp[dataDecl], { + const IntBoolExpData = data(IntExp, { Bool: { value: {} }, IIf: { pred: {}, ifTrue: {}, ifFalse: {} } }) - const IntBoolPrintable = trait(intExp, 'print', { + const IntBoolPrintable = trait(IntExp, 'print', { Bool({ value }) { return value ? 'true' : 'false' }, IIf({ pred, ifTrue, ifFalse }) { return `if (${pred.print()}) { ${ifTrue.print()} } else { ${ifFalse.print()} }` } }) - const IntBoolEvaluable = trait(intExp, 'evaluate', { + const IntBoolEvaluable = trait(IntExp, 'evaluate', { Bool({ value }) { return value }, IIf({ pred, ifTrue, ifFalse }) { return pred.evaluate() ? ifTrue.evaluate() : ifFalse.evaluate() } }) - const intBoolExp = complect(IntBoolExpData, [IntBoolPrintable, IntBoolEvaluable]) + const IntBoolExp = complect(IntBoolExpData, [IntBoolPrintable, IntBoolEvaluable]) - const { Lit, Add, Bool, IIf } = intBoolExp + const { Lit, Add, Bool, IIf } = IntBoolExp() // if (true) { 1 + 2 } else { 3 + 4 } const e = IIf({ @@ -298,7 +302,7 @@ describe('Extensibility for the Masses', () => { }) test("'super' trait call", () => { - const fooData = data({ + const FooData = data({ Foo: { value: String } }) @@ -314,10 +318,10 @@ describe('Extensibility for the Masses', () => { Foo(self) { return `Super: ${BasePrintable[apply](this, self)}` } }) - const fooOverride = complect(fooData, [OverridePrintable]) - const fooSuper = complect(fooData, [SuperPrintable]) + const { Foo: FooOverride } = complect(FooData, [OverridePrintable])() + const { Foo: FooSuper } = complect(FooData, [SuperPrintable])() - expect(fooOverride.Foo('A').print()).toBe('Override: A') - expect(fooSuper.Foo('A').print()).toBe('Super: Base: A') + expect(FooOverride('A').print()).toBe('Override: A') + expect(FooSuper('A').print()).toBe('Super: Base: A') }) }) diff --git a/src/tests/fib.test.mjs b/src/tests/fib.test.mjs index 30bcef4..f784881 100644 --- a/src/tests/fib.test.mjs +++ b/src/tests/fib.test.mjs @@ -21,7 +21,7 @@ describe('Fibonacci trait', () => { ]) }) - const { Fib } = complect(FibData, [Evaluable, PatternEvaluable]) + const { Fib } = complect(FibData, [Evaluable, PatternEvaluable])() test('Wildcard only', () => { expect(Fib).toBeDefined() diff --git a/src/tests/getter.test.mjs b/src/tests/getter.test.mjs index 6fba74d..203b727 100644 --- a/src/tests/getter.test.mjs +++ b/src/tests/getter.test.mjs @@ -1,11 +1,12 @@ -import { data } from "../index.mjs" +import { complect, data } from "../index.mjs" describe('Getter field tests', () => { test('data with computed property names', () => { - const employeeData = data({ + const EmployeeData = data({ Employee: { firstName: {}, lastName: {}, fullName: {} } - }), - { Employee } = employeeData + }) + + const { Employee } = complect(EmployeeData)() const p = Employee({ firstName: 'John', @@ -17,13 +18,13 @@ describe('Getter field tests', () => { }) test('data with self-referential computed property names', () => { - const langData = data({ + const LangData = data({ Alt: { left: {}, right: {} }, Cat: { first: {}, second: {} }, Char: { value: {} }, Empty: {}, }), - { Alt, Empty, Cat, Char } = langData + { Alt, Empty, Cat, Char } = complect(LangData)() // balanced parentheses grammar // S = S ( S ) ∪ ε diff --git a/src/tests/immutability.test.mjs b/src/tests/immutability.test.mjs index 8b1077e..ea99adb 100644 --- a/src/tests/immutability.test.mjs +++ b/src/tests/immutability.test.mjs @@ -9,51 +9,51 @@ describe('Immutability', () => { const PointData = data({ Point: { x: {}, y: {} } }) expect(() => PointData.Point = 1).toThrow(TypeError) - const color = complect(ColorData, []) - expect(() => color.Red = 1).toThrow(TypeError) + const Color = complect(ColorData) + expect(() => Color.Red = 1).toThrow(TypeError) - const point = complect(PointData, []) - expect(() => point.Point = 1).toThrow(TypeError) + const Point = complect(PointData) + expect(() => Point.Point = 1).toThrow(TypeError) }) test('data/complect declaration is not extensible', () => { - const colorData = data({ Red: {}, Green: {}, Blue: {} }) - expect(() => colorData.Purple = []).toThrow(TypeError) + const ColorData = data({ Red: {}, Green: {}, Blue: {} }) + expect(() => ColorData.Purple = []).toThrow(TypeError) - const pointData = data({ Point: { x: {}, y: {} } }) - expect(() => pointData.Point3 = []).toThrow(TypeError) + const PointData = data({ Point: { x: {}, y: {} } }) + expect(() => PointData.Point3 = []).toThrow(TypeError) - const color = complect(colorData, []) - expect(() => color.Purple = []).toThrow(TypeError) + const Color = complect(ColorData) + expect(() => Color.Purple = []).toThrow(TypeError) - const point = complect(pointData, []) - expect(() => point.Point3 = []).toThrow(TypeError) + const Point = complect(PointData) + expect(() => Point.Point3 = []).toThrow(TypeError) }) test('data/complect variant is not extensible', () => { - const colorData = data({ Red: {}, Green: {}, Blue: {} }) - expect(() => colorData.Red.value = '#FF0000').toThrow(TypeError) + const ColorData = data({ Red: {}, Green: {}, Blue: {} }) + expect(() => ColorData.Red.value = '#FF0000').toThrow(TypeError) - const pointData = data({ Point2: { x: {}, y: {} } }), - p = pointData.Point2({ x: 1, y: 2 }) + const PointData = data({ Point2: { x: {}, y: {} } }), + p = PointData().Point2({ x: 1, y: 2 }) expect(() => p.z = 3).toThrow(TypeError) - const color = complect(colorData, []) - expect(() => color.Red.value = '#FF0000').toThrow(TypeError) + const Color = complect(ColorData) + expect(() => Color().Red.value = '#FF0000').toThrow(TypeError) - const point = complect(pointData, []) - expect(() => point.Point2({ x: 1, y: 2 }).z = 3).toThrow(TypeError) + const Point = complect(PointData) + expect(() => Point().Point2({ x: 1, y: 2 }).z = 3).toThrow(TypeError) }) test('data/complect variant properties are immutable', () => { - const pointData = data({ Point2: { x: {}, y: {} } }) - const p = pointData.Point2({ x: 1, y: 2 }) + const PointData = data({ Point2: { x: {}, y: {} } }) + const p = PointData().Point2({ x: 1, y: 2 }) expect(() => p.x = 3).toThrow(TypeError) expect(p.x).toBe(1) - const point = complect(pointData, []) - const p2 = point.Point2({ x: 1, y: 2 }) + const Point = complect(PointData) + const p2 = Point().Point2({ x: 1, y: 2 }) expect(() => p2.x = 3).toThrow(TypeError) expect(p2.x).toBe(1) diff --git a/src/tests/list.test.mjs b/src/tests/list.test.mjs index 45e4697..9d8c1aa 100644 --- a/src/tests/list.test.mjs +++ b/src/tests/list.test.mjs @@ -1,13 +1,18 @@ import { complect, data, trait, _, Pattern } from "../index.mjs"; describe('List tests', () => { - const ListData = data((List, T) => ({ + const ListData = data((T) => ({ Nil: {}, - Cons: { head: T, tail: List(T) } + Cons: { head: T, tail: ListData(T) } })); test('List data', () => { - const { Nil, Cons } = ListData(Number); + const List = complect(ListData), + { Nil, Cons } = List(Number); + + expect(() => + Cons('1', Nil) + ).toThrow(); expect(() => { Cons(1, Cons(2, Cons(3, Nil))); diff --git a/src/tests/memoFix.test.mjs b/src/tests/memoFix.test.mjs index 2e16c48..0e79461 100644 --- a/src/tests/memoFix.test.mjs +++ b/src/tests/memoFix.test.mjs @@ -2,7 +2,7 @@ import { complect, data, memoFix, trait } from '../index.mjs' describe('least fixed point', () => { test('returning bottom on infinite recursion', () => { - const numData = data({ + const NumData = data({ Num: { n: Number } }) @@ -15,7 +15,7 @@ describe('least fixed point', () => { Num({ n }) { return this.Num(n).omegaFix(); } }) - const { Num } = complect(numData, [OmegaTrait, OmegaFixTrait]) + const { Num } = complect(NumData, [OmegaTrait, OmegaFixTrait])() expect(() => Num(2).omega() @@ -25,7 +25,7 @@ describe('least fixed point', () => { }) test('memo performance', () => { - const fibData = data({ + const FibData = data({ Fib: { n: Number } }) @@ -42,17 +42,17 @@ describe('least fixed point', () => { } }) - const { Fib } = complect(fibData, [Evaluable, FixEvalTrait]) + const { Fib } = complect(FibData, [Evaluable, FixEvalTrait])() let start, end; start = performance.now(); - Fib(30).evaluate(); + Fib(20).evaluate(); end = performance.now(); const time = end - start; start = performance.now(); - Fib(30).fixEval(); + Fib(20).fixEval(); end = performance.now(); const memoTime = end - start; @@ -60,7 +60,7 @@ describe('least fixed point', () => { }) test('computed bottom', () => { - const fooData = data({ + const FooData = data({ Foo: { n: Number } }) @@ -72,7 +72,7 @@ describe('least fixed point', () => { return this.Foo(n).foo(); } }) - const { Foo } = complect(fooData, [FooTrait]) + const { Foo } = complect(FooData, [FooTrait])() expect(() => Foo(1).foo()).toThrowError(new Error('Maximum call stack size exceeded')); @@ -86,7 +86,7 @@ describe('least fixed point', () => { } }) - const { Foo: FooFix } = complect(fooData, [FooFixTrait]) + const { Foo: FooFix } = complect(FooData, [FooFixTrait])() expect(FooFix(1).foo()).toBe(19); expect(FooFix(2).foo()).toBe(18); diff --git a/src/tests/pattern-matching.test.mjs b/src/tests/pattern-matching.test.mjs index 648ca94..49dc0b3 100644 --- a/src/tests/pattern-matching.test.mjs +++ b/src/tests/pattern-matching.test.mjs @@ -2,7 +2,7 @@ import { complect, data, trait, _, Pattern } from "../index.mjs"; describe('Pattern matching', () => { test('Simplify expression', () => { - const exprData = data({ + const ExprData = data({ Num: { value: {} }, Var: { name: {} }, Mul: { left: {}, right: {} } @@ -32,8 +32,8 @@ describe('Pattern matching', () => { ]) }) - const expr = complect(exprData, [Simplifyable1, Simplifyable2]), - { Num, Var, Mul } = expr + const Expr = complect(ExprData, [Simplifyable1, Simplifyable2])(), + { Num, Var, Mul } = Expr const e1 = Mul(Var('x'), Num(1)) @@ -57,7 +57,7 @@ describe('Pattern matching', () => { }) test('Notification', () => { - const notificationData = data({ + const NotificationData = data({ Email: { sender: {}, title: {}, body: {} }, SMS: { caller: {}, message: {} }, VoiceRecording: { contactName: {}, link: {} } @@ -69,8 +69,8 @@ describe('Pattern matching', () => { VoiceRecording: ({ contactName, link }) => `You received a voice recording from ${contactName}! Click the link to hear it: ${link}` }) - const notification = complect(notificationData, [Showable]), - { Email, SMS, VoiceRecording } = notification + const Notification = complect(NotificationData, [Showable])(), + { Email, SMS, VoiceRecording } = Notification const sms = SMS('000-0000', 'Call me when you get a chance'), email = Email('Dad@example.com', 'Re: Dinner', 'Did you get my email?'), @@ -82,7 +82,7 @@ describe('Pattern matching', () => { }) test('List', () => { - const ListData = data((List, T) => ({ Nil: {}, Cons: { head: T, tail: List(T) } })) + const ListData = data((T) => ({ Nil: {}, Cons: { head: T, tail: ListData(T) } })) const LengthTrait = trait('length', { Nil: (self) => 0, diff --git a/src/tests/peano.test.mjs b/src/tests/peano.test.mjs index dfc130e..f4bba74 100644 --- a/src/tests/peano.test.mjs +++ b/src/tests/peano.test.mjs @@ -1,13 +1,33 @@ +import { BaseVariant } from "../data.mjs"; import { complect, data, trait } from "../index.mjs"; describe('Peano tests', () => { - const peanoData = data((Peano) => ({ + const PeanoData = data(() => ({ Zero: {}, - Succ: { pred: Peano } + Succ: { pred: PeanoData } })); + test('Peano data', () => { + const Peano = PeanoData(), + { Zero, Succ } = Peano + + const zero = Zero, + one = Succ(zero), + two = Succ(one); + + expect(zero).toBe(Zero); + expect(one.pred).toBe(zero); + expect(two.pred).toBe(one); + + expect(zero).toBeInstanceOf(PeanoData[BaseVariant]) + expect(one).toBeInstanceOf(PeanoData[BaseVariant]) + + expect(() => Succ(1)).toThrow(); + }) + test('Guard test', () => { - const { Zero, Succ } = peanoData; + const Peano = complect(PeanoData)(), + { Zero, Succ } = Peano; expect(Zero).toBeDefined(); expect(Succ).toBeDefined(); @@ -19,6 +39,8 @@ describe('Peano tests', () => { expect(z).toBeDefined(); expect(one).toBeDefined(); expect(two).toBeDefined(); + expect(z).toBeInstanceOf(PeanoData[BaseVariant]) + expect(one).toBeInstanceOf(PeanoData[BaseVariant]) expect(z.pred).toBeUndefined(); expect(one.pred).toBe(z); @@ -32,8 +54,8 @@ describe('Peano tests', () => { Succ({ pred }) { return 1 + pred.value() } }) - const peano = complect(peanoData, [ValueTrait]), - { Zero, Succ } = peano; + const Peano = complect(PeanoData, [ValueTrait]), + { Zero, Succ } = Peano(); test('Peano data', () => { const zero = Zero, diff --git a/src/tests/point.test.mjs b/src/tests/point.test.mjs index c19679f..21b83e5 100644 --- a/src/tests/point.test.mjs +++ b/src/tests/point.test.mjs @@ -1,7 +1,7 @@ import { complect, data, trait } from "../index.mjs" describe('Point tests', () => { - const pointData = data({ + const PointData = data({ Point2: { x: Number, y: Number }, Point3: { x: Number, y: Number, z: Number } }) @@ -11,8 +11,8 @@ describe('Point tests', () => { Point3({ x, y, z }) { return `(${x}, ${y}, ${z})` } }) - const point = complect(pointData, [Printable]), - { Point2, Point3 } = point + const Point = complect(PointData, [Printable]), + { Point2, Point3 } = Point() test('Point data', () => { const p2 = Point2({ x: 1, y: 2 }); diff --git a/src/tests/strictEquality.test.mjs b/src/tests/strictEquality.test.mjs index 86cf1be..b508267 100644 --- a/src/tests/strictEquality.test.mjs +++ b/src/tests/strictEquality.test.mjs @@ -1,20 +1,8 @@ import { complect, data, trait } from "../index.mjs"; describe('Equality tests', () => { - test('data singleton equality', () => { - const colorData = data({ Red: {}, Green: {}, Blue: {} }), - { Red, Green, Blue } = colorData; - - expect(Red).toBe(Red); - expect(Red).toBe(colorData.Red); - expect(Green).toBe(Green); - expect(Green).toBe(colorData.Green); - expect(Blue).toBe(Blue); - expect(Blue).toBe(colorData.Blue); - }) - test('complected equality', () => { - const colorData = data({ Red: {}, Green: {}, Blue: {} }) + const ColorData = data({ Red: {}, Green: {}, Blue: {} }) const Printable = trait('print', { Red: () => 'Red', @@ -22,23 +10,23 @@ describe('Equality tests', () => { Blue: () => 'Blue' }) - const color = complect(colorData, [Printable]), - { Red, Green, Blue } = color; + const Color = complect(ColorData, [Printable])(), + { Red, Green, Blue } = Color; expect(Red).toBe(Red); - expect(Red).toBe(color.Red); + expect(Red).toBe(Color.Red); expect(Green).toBe(Green); - expect(Green).toBe(color.Green); + expect(Green).toBe(Color.Green); expect(Blue).toBe(Blue); - expect(Blue).toBe(color.Blue); + expect(Blue).toBe(Color.Blue); }) test('Point equality', () => { - const pointData = data({ + const PointData = data({ Point2: { x: {}, y: {} }, Point3: { x: {}, y: {}, z: {} } }), - { Point2, Point3 } = pointData; + { Point2, Point3 } = new PointData(); const p2 = Point2({ x: 1, y: 2 }), p3 = Point3({ x: 1, y: 2, z: 3 }); @@ -56,7 +44,7 @@ describe('Equality tests', () => { }) test('complected point equality', () => { - const pointData = data({ + const PointData = data({ Point2: { x: {}, y: {} }, Point3: { x: {}, y: {}, z: {} } }) @@ -66,8 +54,8 @@ describe('Equality tests', () => { Point3: ({ x, y, z }) => `Point3(${x}, ${y}, ${z})` }) - const point = complect(pointData, [Printable]), - { Point2, Point3 } = point; + const Point = complect(PointData, [Printable]), + { Point2, Point3 } = Point(); const p2 = Point2({ x: 1, y: 2 }), p3 = Point3({ x: 1, y: 2, z: 3 }); @@ -85,11 +73,11 @@ describe('Equality tests', () => { }) test('data arithmetic equality', () => { - const expData = data({ + const ExpData = data({ Lit: { value: {} }, Add: { left: {}, right: {} } }), - { Add, Lit } = expData + { Add, Lit } = complect(ExpData)() // 1 + (2 + 3) const exp1 = Add( @@ -131,8 +119,8 @@ describe('Equality tests', () => { }) test('data peano equality', () => { - const peanoData = data({ Zero: {}, Succ: { pred: {} } }), - { Zero, Succ } = peanoData, + const PeanoData = data({ Zero: {}, Succ: { pred: {} } }), + { Zero, Succ } = complect(PeanoData)(), zero = Zero, one = Succ({ pred: zero }), two = Succ({ pred: one }); @@ -149,8 +137,8 @@ describe('Equality tests', () => { }) test('Recursive data equality', () => { - const listData = data({ Nil: {}, Cons: { head: {}, tail: {} } }), - { Cons, Nil } = listData; + const ListData = data({ Nil: {}, Cons: { head: {}, tail: {} } }), + { Cons, Nil } = complect(ListData)(); const list1 = Cons(1, Cons(2, Cons(3, Nil))), list2 = Cons(1, Cons(2, Cons(3, Nil))), @@ -162,11 +150,11 @@ describe('Equality tests', () => { }) test('Array membership', () => { - const pointData = data({ + const PointData = data({ Point2: { x: {}, y: {} }, Point3: { x: {}, y: {}, z: {} } }), - { Point2, Point3 } = pointData; + { Point2, Point3 } = complect(PointData)(); const ps = [Point2(1, 2), Point3(1, 2, 3)]; @@ -179,11 +167,11 @@ describe('Equality tests', () => { }) test('Set membership', () => { - const pointData = data({ + const PointData = data({ Point2: { x: {}, y: {} }, Point3: { x: {}, y: {}, z: {} } }), - { Point2, Point3 } = pointData; + { Point2, Point3 } = complect(PointData)(); const ps = new Set([Point2(1, 2), Point3(1, 2, 3)]); @@ -196,11 +184,11 @@ describe('Equality tests', () => { }) test('Map membership', () => { - const pointData = data({ + const PointData = data({ Point2: { x: {}, y: {} }, Point3: { x: {}, y: {}, z: {} } }), - { Point2, Point3 } = pointData; + { Point2, Point3 } = complect(PointData)(); const ps = new Map([ [Point2(1, 2), 1], diff --git a/src/trait.mjs b/src/trait.mjs index 4ada4b1..121b33b 100644 --- a/src/trait.mjs +++ b/src/trait.mjs @@ -1,8 +1,7 @@ -import { traitDecls } from "./symbols.mjs" +import { dataDecl, traitDecls } from "./symbols.mjs" import { isObjectLiteral } from "./predicates.mjs"; import { BoxedMultiKeyMap } from "./BoxedMultiKeyMap.mjs"; import { Pattern } from "./Pattern.mjs"; -import { Complected } from "./complect.mjs"; export const memoFix = Symbol('memoFix'), apply = Symbol('apply') @@ -65,7 +64,7 @@ export class Trait { * * @overload * Defines a trait - * @param {typeof Trait | typeof Complected} BaseTrait - The base trait to extend + * @param {Constructor} BaseTrait - The base trait to extend * @param {string} methodName - The method name * @param {Cases} cases * @returns {typeof Trait} @@ -79,11 +78,11 @@ export const trait = function (BaseTrait, methodName, cases) { BaseTrait = Trait } - if (BaseTrait.prototype instanceof Complected) { + if (BaseTrait.prototype[dataDecl]) { BaseTrait = BaseTrait.prototype[traitDecls].find(traitDecl => traitDecl.name === methodName); if (!BaseTrait) throw new TypeError(`Trait '${methodName}' not found in Complected`); - } else if (BaseTrait instanceof Complected) { + } else if (BaseTrait[dataDecl]) { BaseTrait = BaseTrait[traitDecls].find(traitDecl => traitDecl.name === methodName); if (!BaseTrait) throw new TypeError(`Trait '${methodName}' not found in Complected`);