Merge pull request 'refactor: Simpler merging mechanism' (#33) from merge_instances into main

Reviewed-on: #33
This commit is contained in:
Glen Whitney 2022-07-29 04:42:05 +00:00
commit 31d3be277d
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'], before: ['Complex'],
test: b => typeof b === 'bigint' test: b => typeof b === 'bigint'
} })
export {BigInt}

View File

@ -1,8 +1,5 @@
export * from './native.mjs' import PocomathInstance from '../core/PocomathInstance.mjs'
export * from '../generic/arithmetic.mjs' import * as bigints from './native.mjs'
import * as generic from '../generic/arithmetic.mjs'
// resolve the conflicts export default PocomathInstance.merge('bigint', bigints, generic)
export {divide} from './divide.mjs'
export {multiply} from './multiply.mjs'
export {sign} from './sign.mjs'
export {sqrt} from './sqrt.mjs'

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 /* 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 * can be any type (for this proof-of-concept; in reality we'd want to
* insist on some numeric or scalar supertype). * 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 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, test: isComplex,
from: { from: {
number: x => ({re: x, im: 0}), number: x => ({re: x, im: 0}),
bigint: x => ({re: x, im: 0n}) bigint: x => ({re: x, im: 0n})
} }
} })
export {Complex}

View File

@ -1,5 +1,5 @@
export * from '../generic/arithmetic.mjs' import PocomathInstance from '../core/PocomathInstance.mjs'
export * from './native.mjs' import * as complexes from './native.mjs'
import * as generic from '../generic/arithmetic.mjs'
// resolve the conflicts export default PocomathInstance.merge('complex', complexes, generic)
export {sqrt} from './sqrt.mjs'

View File

@ -35,7 +35,6 @@ export const sqrt = {
const reSign = sign(z.re) const reSign = sign(z.re)
if (imSign === imZero && reSign === reOne) return self(z.re) if (imSign === imZero && reSign === reOne) return self(z.re)
const reTwo = add(reOne, reOne) const reTwo = add(reOne, reOne)
const partial = add(abs(z), z.re)
return complex( return complex(
multiply(sign(z.im), self(divide(add(abs(z),z.re), reTwo))), multiply(sign(z.im), self(divide(add(abs(z),z.re), reTwo))),
self(divide(subtract(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 dependencyExtractor from './dependencyExtractor.mjs'
import {subsetOfKeys, typesOfSignature} from './utils.mjs' import {subsetOfKeys, typesOfSignature} from './utils.mjs'
const anySpec = {} // fixed dummy specification of 'any' type
export default class PocomathInstance { export default class PocomathInstance {
/* Disallowed names for ops; beware, this is slightly non-DRY /* Disallowed names for ops; beware, this is slightly non-DRY
* in that if a new top-level PocomathInstance method is added, its name * in that if a new top-level PocomathInstance method is added, its name
* must be added to this list. * must be added to this list.
*/ */
static reserved = new Set([ static reserved = new Set([
'config', 'importDependencies', 'install', 'name', 'Types']) 'config',
'importDependencies',
'install',
'installType',
'name',
'Types'])
constructor(name) { constructor(name) {
this.name = name this.name = name
@ -17,7 +24,7 @@ export default class PocomathInstance {
this._affects = {} this._affects = {}
this._typed = typed.create() this._typed = typed.create()
this._typed.clear() 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._doomed = new Set() // for detecting circular reference
this._config = {predictable: false} this._config = {predictable: false}
const self = this const self = this
@ -36,9 +43,19 @@ export default class PocomathInstance {
/** /**
* (Partially) define one or more operations of the instance: * (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 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 * mapping each desired (typed-function) signature to a function taking
* a dependency object to an implementation. * 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 * with the signature in parentheses, e.g. `add(number,number)` to
* refer to just adding two numbers. In this case, it is of course * 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 * necessary to specify an alias to be able to refer to the supplied
* operation in the body of the implementation. [NOTE: this signature- * operation in the body of the implementation.
* 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
*/ */
install(ops) { 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)) { for (const [item, spec] of Object.entries(ops)) {
if (item.slice(0,5) === 'Type_') { if (spec instanceof PocomathInstance) {
this._installType(item.slice(5), spec) this._installInstance(spec)
} else { } 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. /* Used to install a type in a PocomathInstance.
* Note that unlike _installOp below, we can do this immediately *
* @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 (type in this.Types) {
if (spec !== this.Types[type]) { if (spec !== this.Types[type]) {
throw new SyntaxError(`Conflicting definitions of type ${type}`) throw new SyntaxError(`Conflicting definitions of type ${type}`)
@ -165,30 +227,20 @@ export default class PocomathInstance {
} }
/* Used internally by install, see the documentation there */ /* Used internally by install, see the documentation there */
_installOp(name, implementations) { _installFunctions(functions) {
if (name.charAt(0) === '_') { for (const [name, spec] of Object.entries(functions)) {
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 // new implementations, so set the op up to lazily recreate itself
this._invalidate(name) this._invalidate(name)
const opImps = this._imps[name] const opImps = this._imps[name]
for (const [signature, does] of Object.entries(implementations)) { for (const [signature, behavior] of Object.entries(spec)) {
if (signature in opImps) { if (signature in opImps) {
if (does !== opImps[signature].does) { if (behavior.does !== opImps[signature].does) {
throw new SyntaxError( throw new SyntaxError(
`Conflicting definitions of ${signature} for ${name}`) `Conflicting definitions of ${signature} for ${name}`)
} }
} else { } else {
const uses = new Set() opImps[signature] = behavior
does(dependencyExtractor(uses)) for (const dep of behavior.uses) {
opImps[signature] = {uses, does}
for (const dep of uses) {
const depname = dep.split('(', 1)[0] const depname = dep.split('(', 1)[0]
if (depname === 'self') continue if (depname === 'self') continue
this._addAffect(depname, name) this._addAffect(depname, name)
@ -199,6 +251,7 @@ export default class PocomathInstance {
} }
} }
} }
}
_addAffect(dependency, dependent) { _addAffect(dependency, dependent) {
if (dependency in this._affects) { if (dependency in this._affects) {

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'], before: ['Complex'],
test: n => typeof n === 'number', test: n => typeof n === 'number',
from: {string: s => +s} from: {string: s => +s}
} })
export {Number}

View File

@ -1,6 +1,6 @@
export * from '../generic/arithmetic.mjs' import PocomathInstance from '../core/PocomathInstance.mjs'
export * from './native.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 complex from './complex/native.mjs'
import * as generic from './generic/all.mjs' import * as generic from './generic/all.mjs'
const math = new PocomathInstance('math') const math = PocomathInstance.merge('math', numbers, bigints, complex, generic)
math.install(numbers)
math.install(bigints)
math.install(complex)
math.install(generic)
export default math export default math

View File

@ -18,14 +18,14 @@ describe('The default full pocomath instance "math"', () => {
}) })
it('can be extended', () => { it('can be extended', () => {
math.installType('stringK', {
test: s => typeof s === 'string' && s.charAt(0) === 'K',
before: ['string']
})
math.install({ math.install({
add: { add: {
'...stringK': () => addends => addends.reduce((x,y) => x+y, '') '...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') 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", () => { it("can be assembled in any order", () => {
bw.install(numbers) 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.subtract(16, bw.add(3,4,2)), 7)
assert.strictEqual(bw.negate('8'), -8) assert.strictEqual(bw.negate('8'), -8)
assert.deepStrictEqual(bw.add(bw.complex(1,3), 1), {re: 2, im: 3}) assert.deepStrictEqual(bw.add(bw.complex(1,3), 1), {re: 2, im: 3})