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'],
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})