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})