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.
This commit is contained in:
Glen Whitney 2022-07-27 22:28:40 -07:00
parent 58fa661a2d
commit d9d7af961f
12 changed files with 146 additions and 89 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<string, Object<Signature, ({deps})=> 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<string,
* PocomathInstance
* | Object<Signature, ({deps})=> 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<string, <that type> => <type name>> // 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)
}
}
}
}

View File

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

View File

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

View File

@ -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'

View File

@ -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

View File

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

View File

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