From d9d7af961f8710ed764fbca863a710022456d4ce Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Wed, 27 Jul 2022 22:28:40 -0700 Subject: [PATCH] refactor: Simpler merging mechanism Merging of Pocomath modules is eased by allowing one PocomathInstance to be merged into another. That allows types, for example, to be exported as a PocomathInstance (so there is no need for a special identifier convention for types; they can be directly added with an installType method). Also, larger modules can just be exported as an instance, since there is more flexibility and more checking in merging PocomathInstances than raw modules. --- src/bigint/Types/bigint.mjs | 7 +- src/bigint/all.mjs | 11 +-- src/complex/Types/Complex.mjs | 8 +- src/complex/all.mjs | 8 +- src/complex/sqrt.mjs | 1 - src/core/PocomathInstance.mjs | 161 ++++++++++++++++++++++------------ src/generic/Types/generic.mjs | 5 +- src/number/Types/number.mjs | 8 +- src/number/all.mjs | 10 +-- src/pocomath.mjs | 6 +- test/_pocomath.mjs | 8 +- test/custom.mjs | 2 +- 12 files changed, 146 insertions(+), 89 deletions(-) diff --git a/src/bigint/Types/bigint.mjs b/src/bigint/Types/bigint.mjs index c1416af..2d320ad 100644 --- a/src/bigint/Types/bigint.mjs +++ b/src/bigint/Types/bigint.mjs @@ -1,4 +1,7 @@ -export const Type_bigint = { +import PocomathInstance from '../../core/PocomathInstance.mjs' +const BigInt = new PocomathInstance('BigInt') +BigInt.installType('bigint', { before: ['Complex'], test: b => typeof b === 'bigint' -} +}) +export {BigInt} diff --git a/src/bigint/all.mjs b/src/bigint/all.mjs index 0f87414..0d6e517 100644 --- a/src/bigint/all.mjs +++ b/src/bigint/all.mjs @@ -1,8 +1,5 @@ -export * from './native.mjs' -export * from '../generic/arithmetic.mjs' +import PocomathInstance from '../core/PocomathInstance.mjs' +import * as bigints from './native.mjs' +import * as generic from '../generic/arithmetic.mjs' -// resolve the conflicts -export {divide} from './divide.mjs' -export {multiply} from './multiply.mjs' -export {sign} from './sign.mjs' -export {sqrt} from './sqrt.mjs' +export default PocomathInstance.merge('bigint', bigints, generic) diff --git a/src/complex/Types/Complex.mjs b/src/complex/Types/Complex.mjs index 30bbf3f..05fe622 100644 --- a/src/complex/Types/Complex.mjs +++ b/src/complex/Types/Complex.mjs @@ -1,3 +1,5 @@ +import PocomathInstance from '../../core/PocomathInstance.mjs' + /* Use a plain object with keys re and im for a complex; note the components * can be any type (for this proof-of-concept; in reality we'd want to * insist on some numeric or scalar supertype). @@ -6,11 +8,13 @@ function isComplex(z) { return z && typeof z === 'object' && 're' in z && 'im' in z } -export const Type_Complex = { +const Complex = new PocomathInstance('Complex') +Complex.installType('Complex', { test: isComplex, from: { number: x => ({re: x, im: 0}), bigint: x => ({re: x, im: 0n}) } -} +}) +export {Complex} diff --git a/src/complex/all.mjs b/src/complex/all.mjs index ddb7679..5d9f3b3 100644 --- a/src/complex/all.mjs +++ b/src/complex/all.mjs @@ -1,5 +1,5 @@ -export * from '../generic/arithmetic.mjs' -export * from './native.mjs' +import PocomathInstance from '../core/PocomathInstance.mjs' +import * as complexes from './native.mjs' +import * as generic from '../generic/arithmetic.mjs' -// resolve the conflicts -export {sqrt} from './sqrt.mjs' +export default PocomathInstance.merge('complex', complexes, generic) diff --git a/src/complex/sqrt.mjs b/src/complex/sqrt.mjs index cba7361..c3c3ab5 100644 --- a/src/complex/sqrt.mjs +++ b/src/complex/sqrt.mjs @@ -35,7 +35,6 @@ export const sqrt = { const reSign = sign(z.re) if (imSign === imZero && reSign === reOne) return self(z.re) const reTwo = add(reOne, reOne) - const partial = add(abs(z), z.re) return complex( multiply(sign(z.im), self(divide(add(abs(z),z.re), reTwo))), self(divide(subtract(abs(z),z.re), reTwo)) diff --git a/src/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index 7462082..405479f 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -3,13 +3,20 @@ import typed from 'typed-function' import dependencyExtractor from './dependencyExtractor.mjs' import {subsetOfKeys, typesOfSignature} from './utils.mjs' +const anySpec = {} // fixed dummy specification of 'any' type + export default class PocomathInstance { /* Disallowed names for ops; beware, this is slightly non-DRY * in that if a new top-level PocomathInstance method is added, its name * must be added to this list. */ static reserved = new Set([ - 'config', 'importDependencies', 'install', 'name', 'Types']) + 'config', + 'importDependencies', + 'install', + 'installType', + 'name', + 'Types']) constructor(name) { this.name = name @@ -17,7 +24,7 @@ export default class PocomathInstance { this._affects = {} this._typed = typed.create() this._typed.clear() - this.Types = {any: {}} // dummy entry to track the default 'any' type + this.Types = {any: anySpec} // dummy entry to track the default 'any' type this._doomed = new Set() // for detecting circular reference this._config = {predictable: false} const self = this @@ -36,9 +43,19 @@ export default class PocomathInstance { /** * (Partially) define one or more operations of the instance: * - * @param {Object implementation>>} ops + * The sole parameter can be another Pocomath instance, in which case all + * of the types and operations of the other instance are installed in this + * one, or it can be a plain object as described below. + * + * @param {Object implementation>>} ops * The only parameter ops gives the semantics of the operations to install. - * The keys are operation names. The value for a key is an object + * The keys are operation names. The value for a key could be + * a PocomathInstance, in which case it is simply merged into this + * instance. + * + * Otherwise, ops must be an object * mapping each desired (typed-function) signature to a function taking * a dependency object to an implementation. * @@ -63,29 +80,55 @@ export default class PocomathInstance { * with the signature in parentheses, e.g. `add(number,number)` to * refer to just adding two numbers. In this case, it is of course * necessary to specify an alias to be able to refer to the supplied - * operation in the body of the implementation. [NOTE: this signature- - * specific reference is not yet implemented.] - * - * Note that any "operation" whose name begins with `Type_` is special: - * it defines a types that must be installed in the instance. - * The remainder of the "operation" name following the `_` is the - * name of the type. The value of the "operation" should be a plain - * object with the following properties: - * - * - test: the predicate for the type - * - from: a plain object mapping the names of types that can be converted - * **to** this type to the corresponding conversion functions - * - before: [optional] a list of types this should be added - * before, in priority order + * operation in the body of the implementation. */ install(ops) { + if (ops instanceof PocomathInstance) { + return _installInstance(ops) + } + /* Standardize the format of all implementations, weeding out + * any other instances as we go + */ + const stdFunctions = {} for (const [item, spec] of Object.entries(ops)) { - if (item.slice(0,5) === 'Type_') { - this._installType(item.slice(5), spec) + if (spec instanceof PocomathInstance) { + this._installInstance(spec) } else { - this._installOp(item, spec) + if (item.charAt(0) === '_') { + throw new SyntaxError( + `Pocomath: Cannot install ${item}, ` + + 'initial _ reserved for internal use.') + } + if (PocomathInstance.reserved.has(item)) { + throw new SyntaxError( + `Pocomath: reserved function '${item}' cannot be modified.`) + } + const stdimps = {} + for (const [signature, does] of Object.entries(spec)) { + const uses = new Set() + does(dependencyExtractor(uses)) + stdimps[signature] = {uses, does} + } + stdFunctions[item] = stdimps } } + this._installFunctions(stdFunctions) + } + + /* Merge any number of PocomathInstances or modules: */ + static merge(name, ...pieces) { + const result = new PocomathInstance(name) + for (const piece of pieces) { + result.install(piece) + } + return result + } + + _installInstance(other) { + for (const [type, spec] of Object.entries(other.Types)) { + this.installType(type, spec) + } + this._installFunctions(other._imps) } /** @@ -127,10 +170,29 @@ export default class PocomathInstance { } } - /* Used internally by install, see the documentation there. - * Note that unlike _installOp below, we can do this immediately + /* Used to install a type in a PocomathInstance. + * + * @param {string} name The name of the type + * @param {{test: any => bool, // the predicate for the type + * from: Record => > // conversions + * before: string[] // lower priority types + * }} specification + * + * The second parameter of this function specifies the structure of the + * type via a plain + * object with the following properties: + * + * - test: the predicate for the type + * - from: a plain object mapping the names of types that can be converted + * **to** this type to the corresponding conversion functions + * - before: [optional] a list of types this should be added + * before, in priority order */ - _installType(type, spec) { + /* + * Implementation note: unlike _installFunctions below, we can make + * the corresponding changes to the _typed object immediately + */ + installType(type, spec) { if (type in this.Types) { if (spec !== this.Types[type]) { throw new SyntaxError(`Conflicting definitions of type ${type}`) @@ -165,36 +227,27 @@ export default class PocomathInstance { } /* Used internally by install, see the documentation there */ - _installOp(name, implementations) { - if (name.charAt(0) === '_') { - throw new SyntaxError( - `Pocomath: Cannot install ${name}, ` - + 'initial _ reserved for internal use.') - } - if (PocomathInstance.reserved.has(name)) { - throw new SyntaxError( - `Pocomath: the meaning of function '${name}' cannot be modified.`) - } - // new implementations, so set the op up to lazily recreate itself - this._invalidate(name) - const opImps = this._imps[name] - for (const [signature, does] of Object.entries(implementations)) { - if (signature in opImps) { - if (does !== opImps[signature].does) { - throw new SyntaxError( - `Conflicting definitions of ${signature} for ${name}`) - } - } else { - const uses = new Set() - does(dependencyExtractor(uses)) - opImps[signature] = {uses, does} - for (const dep of uses) { - const depname = dep.split('(', 1)[0] - if (depname === 'self') continue - this._addAffect(depname, name) - } - for (const type of typesOfSignature(signature)) { - this._addAffect(':' + type, name) + _installFunctions(functions) { + for (const [name, spec] of Object.entries(functions)) { + // new implementations, so set the op up to lazily recreate itself + this._invalidate(name) + const opImps = this._imps[name] + for (const [signature, behavior] of Object.entries(spec)) { + if (signature in opImps) { + if (behavior.does !== opImps[signature].does) { + throw new SyntaxError( + `Conflicting definitions of ${signature} for ${name}`) + } + } else { + opImps[signature] = behavior + for (const dep of behavior.uses) { + const depname = dep.split('(', 1)[0] + if (depname === 'self') continue + this._addAffect(depname, name) + } + for (const type of typesOfSignature(signature)) { + this._addAffect(':' + type, name) + } } } } diff --git a/src/generic/Types/generic.mjs b/src/generic/Types/generic.mjs index 473ec56..33842f2 100644 --- a/src/generic/Types/generic.mjs +++ b/src/generic/Types/generic.mjs @@ -1,2 +1,5 @@ -export const Type_undefined = {test: u => u === undefined} +import PocomathInstance from '../../core/PocomathInstance.mjs' +const Undefined = new PocomathInstance('Undefined') +Undefined.installType('undefined', {test: u => u === undefined}) +export {Undefined} diff --git a/src/number/Types/number.mjs b/src/number/Types/number.mjs index f8179fc..e068f67 100644 --- a/src/number/Types/number.mjs +++ b/src/number/Types/number.mjs @@ -1,6 +1,8 @@ -export const Type_number = { +import PocomathInstance from '../../core/PocomathInstance.mjs' +const Number = new PocomathInstance('Number') +Number.installType('number', { before: ['Complex'], test: n => typeof n === 'number', from: {string: s => +s} -} - +}) +export {Number} diff --git a/src/number/all.mjs b/src/number/all.mjs index ef8823e..54db15a 100644 --- a/src/number/all.mjs +++ b/src/number/all.mjs @@ -1,6 +1,6 @@ -export * from '../generic/arithmetic.mjs' -export * from './native.mjs' +import PocomathInstance from '../core/PocomathInstance.mjs' +import * as numbers from './native.mjs' +import * as generic from '../generic/arithmetic.mjs' + +export default PocomathInstance.merge('number', numbers, generic) -// resolve the conflicts -export {sqrt} from './sqrt.mjs' -export {multiply} from './multiply.mjs' diff --git a/src/pocomath.mjs b/src/pocomath.mjs index 6b61f4f..65f8103 100644 --- a/src/pocomath.mjs +++ b/src/pocomath.mjs @@ -5,10 +5,6 @@ import * as bigints from './bigint/native.mjs' import * as complex from './complex/native.mjs' import * as generic from './generic/all.mjs' -const math = new PocomathInstance('math') -math.install(numbers) -math.install(bigints) -math.install(complex) -math.install(generic) +const math = PocomathInstance.merge('math', numbers, bigints, complex, generic) export default math diff --git a/test/_pocomath.mjs b/test/_pocomath.mjs index e7b30e0..a361c31 100644 --- a/test/_pocomath.mjs +++ b/test/_pocomath.mjs @@ -18,14 +18,14 @@ describe('The default full pocomath instance "math"', () => { }) it('can be extended', () => { + math.installType('stringK', { + test: s => typeof s === 'string' && s.charAt(0) === 'K', + before: ['string'] + }) math.install({ add: { '...stringK': () => addends => addends.reduce((x,y) => x+y, '') }, - Type_stringK: { - test: s => typeof s === 'string' && s.charAt(0) === 'K', - before: ['string'] - } }) assert.strictEqual(math.add('Kilroy','K is here'), 'KilroyK is here') }) diff --git a/test/custom.mjs b/test/custom.mjs index e057dbf..9292437 100644 --- a/test/custom.mjs +++ b/test/custom.mjs @@ -23,7 +23,7 @@ describe('A custom instance', () => { it("can be assembled in any order", () => { bw.install(numbers) - bw.install({Type_string: {test: s => typeof s === 'string'}}) + bw.installType('string', {test: s => typeof s === 'string'}) assert.strictEqual(bw.subtract(16, bw.add(3,4,2)), 7) assert.strictEqual(bw.negate('8'), -8) assert.deepStrictEqual(bw.add(bw.complex(1,3), 1), {re: 2, im: 3})