fix: Separate typed instance for each PocomathInstance (#15)
Also starts each PocomathInstance with no types at all, and uses the new situation to eliminate the need for a Complex "base case". Resolves #14. Resolves #13. Co-authored-by: Glen Whitney <glen@studioinfinity.org> Reviewed-on: #15
This commit is contained in:
parent
ed71b15969
commit
0069597a76
25 changed files with 120 additions and 74 deletions
3
src/bigint/Types/bigint.mjs
Normal file
3
src/bigint/Types/bigint.mjs
Normal file
|
@ -0,0 +1,3 @@
|
|||
export const Types = {
|
||||
bigint: {test: b => typeof b === 'bigint'}
|
||||
}
|
5
src/bigint/add.mjs
Normal file
5
src/bigint/add.mjs
Normal file
|
@ -0,0 +1,5 @@
|
|||
export {Types} from './Types/bigint.mjs'
|
||||
|
||||
export const add = {
|
||||
'...bigint': [[], addends => addends.reduce((x,y) => x+y, 0n)],
|
||||
}
|
4
src/bigint/all.mjs
Normal file
4
src/bigint/all.mjs
Normal file
|
@ -0,0 +1,4 @@
|
|||
export {Types} from './Types/bigint.mjs'
|
||||
export {add} from './add.mjs'
|
||||
export {negate} from './negate.mjs'
|
||||
export {subtract} from '../generic/subtract.mjs'
|
3
src/bigint/negate.mjs
Normal file
3
src/bigint/negate.mjs
Normal file
|
@ -0,0 +1,3 @@
|
|||
export {Types} from './Types/bigint.mjs'
|
||||
|
||||
export const negate = {bigint: [[], b => -b ]}
|
20
src/complex/Types/Complex.mjs
Normal file
20
src/complex/Types/Complex.mjs
Normal file
|
@ -0,0 +1,20 @@
|
|||
/* 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).
|
||||
*/
|
||||
export function isComplex(z) {
|
||||
return z && typeof z === 'object' && 're' in z && 'im' in z
|
||||
}
|
||||
|
||||
export const Types = {
|
||||
Complex: {
|
||||
test: isComplex,
|
||||
number: x => ({re: x, im: 0}),
|
||||
bigint: x => ({re: x, im: 0n})
|
||||
}
|
||||
}
|
||||
|
||||
/* test if an entity is Complex<number>, so to speak: */
|
||||
export function numComplex(z) {
|
||||
return isComplex(z) && typeof z.re === 'number' && typeof z.im === 'number'
|
||||
}
|
10
src/complex/add.mjs
Normal file
10
src/complex/add.mjs
Normal file
|
@ -0,0 +1,10 @@
|
|||
export {Types} from './Types/Complex.mjs'
|
||||
|
||||
export const add = {
|
||||
'...Complex': [['self'], ref => addends => {
|
||||
if (addends.length === 0) return {re:0, im:0}
|
||||
const seed = addends.shift()
|
||||
return addends.reduce((w,z) =>
|
||||
({re: ref.self(w.re, z.re), im: ref.self(w.im, z.im)}), seed)
|
||||
}]
|
||||
}
|
5
src/complex/all.mjs
Normal file
5
src/complex/all.mjs
Normal file
|
@ -0,0 +1,5 @@
|
|||
export {Types} from './Types/Complex.mjs'
|
||||
export {complex} from './complex.mjs'
|
||||
export {add} from './add.mjs'
|
||||
export {negate} from './negate.mjs'
|
||||
export {subtract} from '../generic/subtract.mjs'
|
11
src/complex/complex.mjs
Normal file
11
src/complex/complex.mjs
Normal file
|
@ -0,0 +1,11 @@
|
|||
export {Types} from './Types/Complex.mjs'
|
||||
|
||||
export const complex = {
|
||||
/* Very permissive for sake of proof-of-concept; would be better to
|
||||
* have a numeric/scalar type, e.g. by implementing subtypes in
|
||||
* typed-function
|
||||
*/
|
||||
'any, any': [[], (x, y) => ({re: x, im: y})],
|
||||
/* Take advantage of conversions in typed-function */
|
||||
Complex: [[], z => z]
|
||||
}
|
18
src/complex/extendToComplex.mjs
Normal file
18
src/complex/extendToComplex.mjs
Normal file
|
@ -0,0 +1,18 @@
|
|||
import * as complex from './complex.mjs'
|
||||
|
||||
/* Add all the complex implementations for functions already
|
||||
in the instance:
|
||||
*/
|
||||
|
||||
export default async function extendToComplex(pmath) {
|
||||
pmath.install(complex)
|
||||
for (const name in pmath._imps) {
|
||||
const modulePath = `./${name}.mjs`
|
||||
try {
|
||||
const mod = await import(modulePath)
|
||||
pmath.install(mod)
|
||||
} catch (err) {
|
||||
// Guess it wasn't a method available in complex; no worries
|
||||
}
|
||||
}
|
||||
}
|
7
src/complex/negate.mjs
Normal file
7
src/complex/negate.mjs
Normal file
|
@ -0,0 +1,7 @@
|
|||
export {Types} from './Types/Complex.mjs'
|
||||
|
||||
export const negate = {
|
||||
Complex: [['self'], ref => z => {
|
||||
return {re: ref.self(z.re), im: ref.self(z.im)}
|
||||
}]
|
||||
}
|
178
src/core/PocomathInstance.mjs
Normal file
178
src/core/PocomathInstance.mjs
Normal file
|
@ -0,0 +1,178 @@
|
|||
/* Core of pocomath: create an instance */
|
||||
import typed from 'typed-function'
|
||||
|
||||
export default class PocomathInstance {
|
||||
constructor(name) {
|
||||
this.name = name
|
||||
this._imps = {}
|
||||
this._affects = {}
|
||||
this._typed = typed.create()
|
||||
this._typed.clear()
|
||||
// Convenient hack for now, would remove when a real string type is added:
|
||||
this._typed.addTypes([{name: 'string', test: s => typeof s === 'string'}])
|
||||
}
|
||||
|
||||
/**
|
||||
* (Partially) define one or more operations of the instance:
|
||||
*
|
||||
* @param {Object<string, Object<Signature, [string[], function]>>} 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
|
||||
* mapping (typed-function) signature strings to pairs of dependency
|
||||
* lists and implementation functions.
|
||||
*
|
||||
* A dependency list is a list of strings. Each string can either be the
|
||||
* name of a function that the corresponding implementation has to call,
|
||||
* or a specification of a particular signature of a function that it has
|
||||
* to call, in the form 'FN(SIGNATURE)'. Note the function name can be
|
||||
* the special value 'self' to indicate a recursive call to the given
|
||||
* operation (either with or without a particular signature.
|
||||
*
|
||||
* There are two cases for the implementation function. If the dependency
|
||||
* list is empty, it should be a function taking the arguments specified
|
||||
* by the signature and returning the value. Otherwise, it should be
|
||||
* a function taking an object with the dependency lists as keys and the
|
||||
* requested functions as values, to a function taking the arguments
|
||||
* specified by the signature and returning the value.
|
||||
*
|
||||
* Note that the "operation" named `Types` is special: it gives
|
||||
* types that must be installed in the instance. In this case, the keys
|
||||
* are type names, and the values are objects with a property 'test'
|
||||
* giving the predicate for the type, and properties for each type that can
|
||||
* be converted **to** this type, giving the corresponding conversion
|
||||
* function.
|
||||
*/
|
||||
install(ops) {
|
||||
for (const key in ops) this._installOp(key, ops[key])
|
||||
}
|
||||
|
||||
/* Used internally by install, see the documentation there */
|
||||
_installOp(name, implementations) {
|
||||
// new implementations, so set the op up to lazily recreate itself
|
||||
this._invalidate(name)
|
||||
const opImps = this._imps[name]
|
||||
for (const signature in implementations) {
|
||||
if (signature in opImps) {
|
||||
if (implementations[signature] === opImps[signature]) continue
|
||||
throw new SyntaxError(
|
||||
`Conflicting definitions of ${signature} for ${name}`)
|
||||
} else {
|
||||
opImps[signature] = implementations[signature]
|
||||
for (const dep of implementations[signature][0] || []) {
|
||||
const depname = dep.split('(', 1)[0]
|
||||
if (depname === 'self') continue
|
||||
if (!(depname in this._affects)) {
|
||||
this._affects[depname] = new Set()
|
||||
}
|
||||
this._affects[depname].add(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset an operation to require creation of typed-function,
|
||||
* and if it has no implementations so far, set them up.
|
||||
*/
|
||||
_invalidate(name) {
|
||||
const self = this
|
||||
this[name] = function () {
|
||||
return self._bundle(name).apply(self, arguments)
|
||||
}
|
||||
if (!(name in this._imps)) {
|
||||
this._imps[name] = {}
|
||||
}
|
||||
if (name in this._affects) {
|
||||
for (const ancestor of this._affects[name]) {
|
||||
this._invalidate(ancestor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a typed-function from the signatures for the given name and
|
||||
* assign it to the property with that name, returning it as well
|
||||
*/
|
||||
_bundle(name) {
|
||||
const imps = this._imps[name]
|
||||
if (!imps || Object.keys(imps).length === 0) {
|
||||
throw new SyntaxError(`No implementations for ${name}`)
|
||||
}
|
||||
this._ensureTypes()
|
||||
const tf_imps = {}
|
||||
for (const signature in imps) {
|
||||
const [deps, imp] = imps[signature]
|
||||
if (deps.length === 0) {
|
||||
tf_imps[signature] = imp
|
||||
} else {
|
||||
const refs = {}
|
||||
let self_referential = false
|
||||
for (const dep of deps) {
|
||||
// TODO: handle signature-specific dependencies
|
||||
if (dep.includes('(')) {
|
||||
throw new Error('signature specific reference unimplemented')
|
||||
}
|
||||
if (dep === 'self') {
|
||||
self_referential = true
|
||||
} else {
|
||||
refs[dep] = this._ensureBundle(dep) // assume acyclic for now
|
||||
}
|
||||
}
|
||||
if (self_referential) {
|
||||
tf_imps[signature] = this._typed.referToSelf(self => {
|
||||
refs.self = self
|
||||
return imp(refs)
|
||||
})
|
||||
} else {
|
||||
tf_imps[signature] = imp(refs)
|
||||
}
|
||||
}
|
||||
}
|
||||
const tf = this._typed(name, tf_imps)
|
||||
this[name] = tf
|
||||
return tf
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the generated typed function is assigned to the given
|
||||
* name and return it
|
||||
*/
|
||||
_ensureBundle(name) {
|
||||
const maybe = this[name]
|
||||
if (this._typed.isTypedFunction(maybe)) return maybe
|
||||
return this._bundle(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that all of the requested types and conversions are actually
|
||||
* in the typed-function universe:
|
||||
*/
|
||||
_ensureTypes() {
|
||||
const newTypes = []
|
||||
const newTypeSet = new Set()
|
||||
const knownTypeSet = new Set()
|
||||
const conversions = []
|
||||
const typeSpec = this._imps.Types
|
||||
for (const name in this._imps.Types) {
|
||||
knownTypeSet.add(name)
|
||||
for (const from in typeSpec[name]) {
|
||||
if (from === 'test') continue;
|
||||
conversions.push(
|
||||
{from, to: name, convert: typeSpec[name][from]})
|
||||
}
|
||||
try { // Hack: work around typed-function #154
|
||||
this._typed._findType(name)
|
||||
} catch {
|
||||
newTypeSet.add(name)
|
||||
newTypes.push({name, test: typeSpec[name].test})
|
||||
}
|
||||
}
|
||||
this._typed.addTypes(newTypes)
|
||||
const newConversions = conversions.filter(
|
||||
item => (newTypeSet.has(item.from) || newTypeSet.has(item.to)) &&
|
||||
knownTypeSet.has(item.from) && knownTypeSet.has(item.to)
|
||||
)
|
||||
this._typed.addConversions(newConversions)
|
||||
}
|
||||
|
||||
}
|
3
src/generic/subtract.mjs
Normal file
3
src/generic/subtract.mjs
Normal file
|
@ -0,0 +1,3 @@
|
|||
export const subtract = {
|
||||
'any,any': [['add', 'negate'], ref => (x,y) => ref.add(x, ref.negate(y))]
|
||||
}
|
7
src/number/Types/number.mjs
Normal file
7
src/number/Types/number.mjs
Normal file
|
@ -0,0 +1,7 @@
|
|||
export const Types = {
|
||||
number: {
|
||||
test: n => typeof n === 'number',
|
||||
string: s => +s
|
||||
}
|
||||
}
|
||||
|
5
src/number/add.mjs
Normal file
5
src/number/add.mjs
Normal file
|
@ -0,0 +1,5 @@
|
|||
export {Types} from './Types/number.mjs'
|
||||
|
||||
export const add = {
|
||||
'...number': [[], addends => addends.reduce((x,y) => x+y, 0)],
|
||||
}
|
4
src/number/all.mjs
Normal file
4
src/number/all.mjs
Normal file
|
@ -0,0 +1,4 @@
|
|||
export {Types} from './Types/number.mjs'
|
||||
export {add} from './add.mjs'
|
||||
export {negate} from './negate.mjs'
|
||||
export {subtract} from '../generic/subtract.mjs'
|
3
src/number/negate.mjs
Normal file
3
src/number/negate.mjs
Normal file
|
@ -0,0 +1,3 @@
|
|||
export { Types } from './Types/number.mjs'
|
||||
|
||||
export const negate = {number: [[], n => -n]}
|
12
src/pocomath.mjs
Normal file
12
src/pocomath.mjs
Normal file
|
@ -0,0 +1,12 @@
|
|||
/* Core of pocomath: generates the default instance */
|
||||
import PocomathInstance from './core/PocomathInstance.mjs'
|
||||
import * as numbers from './number/all.mjs'
|
||||
import * as bigints from './bigint/all.mjs'
|
||||
import * as complex from './complex/all.mjs'
|
||||
|
||||
const math = new PocomathInstance('math')
|
||||
math.install(numbers)
|
||||
math.install(bigints)
|
||||
math.install(complex)
|
||||
|
||||
export default math
|
Loading…
Add table
Add a link
Reference in a new issue