feat: Allow self-reference in implementations (#4)
This PR also uses such self-reference to define negate and add for Complex numbers in a way that is independent of component types. Also adds a bigint type and verifies that pocomath will then handle Gaussian integers "for free". Ensures that if one function is invalidated, then any that depend on it will be. Co-authored-by: Glen Whitney <glen@studioinfinity.org> Reviewed-on: #4
This commit is contained in:
parent
77b04fbdbb
commit
84a8b9d5c4
@ -5,6 +5,7 @@ export default class PocomathInstance {
|
|||||||
constructor(name) {
|
constructor(name) {
|
||||||
this.name = name
|
this.name = name
|
||||||
this._imps = {}
|
this._imps = {}
|
||||||
|
this._affects = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -41,11 +42,19 @@ export default class PocomathInstance {
|
|||||||
const opImps = this._imps[name]
|
const opImps = this._imps[name]
|
||||||
for (const signature in implementations) {
|
for (const signature in implementations) {
|
||||||
if (signature in opImps) {
|
if (signature in opImps) {
|
||||||
if (implemenatations[signature] === opImps[signature]) continue
|
if (implementations[signature] === opImps[signature]) continue
|
||||||
throw new SyntaxError(
|
throw new SyntaxError(
|
||||||
`Conflicting definitions of ${signature} for ${name}`)
|
`Conflicting definitions of ${signature} for ${name}`)
|
||||||
} else {
|
} else {
|
||||||
opImps[signature] = implementations[signature]
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -62,6 +71,11 @@ export default class PocomathInstance {
|
|||||||
if (!(name in this._imps)) {
|
if (!(name in this._imps)) {
|
||||||
this._imps[name] = {}
|
this._imps[name] = {}
|
||||||
}
|
}
|
||||||
|
if (name in this._affects) {
|
||||||
|
for (const ancestor of this._affects[name]) {
|
||||||
|
this._invalidate(ancestor)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -80,18 +94,26 @@ export default class PocomathInstance {
|
|||||||
tf_imps[signature] = imp
|
tf_imps[signature] = imp
|
||||||
} else {
|
} else {
|
||||||
const refs = {}
|
const refs = {}
|
||||||
|
let self_referential = false
|
||||||
for (const dep of deps) {
|
for (const dep of deps) {
|
||||||
// TODO: handle self dependencies
|
|
||||||
if (dep.slice(0,4) === 'self') {
|
|
||||||
throw new Error('self-reference unimplemented')
|
|
||||||
}
|
|
||||||
// TODO: handle signature-specific dependencies
|
// TODO: handle signature-specific dependencies
|
||||||
if (dep.includes('(')) {
|
if (dep.includes('(')) {
|
||||||
throw new Error('signature specific reference unimplemented')
|
throw new Error('signature specific reference unimplemented')
|
||||||
}
|
}
|
||||||
refs[dep] = this._ensureBundle(dep) // just assume acyclic for now
|
if (dep === 'self') {
|
||||||
|
self_referential = true
|
||||||
|
} else {
|
||||||
|
refs[dep] = this._ensureBundle(dep) // assume acyclic for now
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (self_referential) {
|
||||||
|
tf_imps[signature] = typed.referToSelf(self => {
|
||||||
|
refs.self = self
|
||||||
|
return imp(refs)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
tf_imps[signature] = imp(refs)
|
||||||
}
|
}
|
||||||
tf_imps[signature] = imp(refs)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const tf = typed(name, tf_imps)
|
const tf = typed(name, tf_imps)
|
||||||
|
3
bigint/BigInt.mjs
Normal file
3
bigint/BigInt.mjs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import typed from 'typed-function'
|
||||||
|
|
||||||
|
typed.addType({name: 'bigint', test: b => typeof b === 'bigint'})
|
4
bigint/add.mjs
Normal file
4
bigint/add.mjs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import './BigInt.mjs'
|
||||||
|
export const add = {
|
||||||
|
'...bigint': [[], addends => addends.reduce((x,y) => x+y, 0n)],
|
||||||
|
}
|
3
bigint/all.mjs
Normal file
3
bigint/all.mjs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export {add} from './add.mjs'
|
||||||
|
export {negate} from './negate.mjs'
|
||||||
|
export {subtract} from '../generic/subtract.mjs'
|
2
bigint/negate.mjs
Normal file
2
bigint/negate.mjs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import './BigInt.mjs'
|
||||||
|
export const negate = {bigint: [[], b => -b ]}
|
@ -1,14 +1,29 @@
|
|||||||
import typed from 'typed-function'
|
import typed from 'typed-function'
|
||||||
|
|
||||||
/* Use a plain object with keys re and im for a complex */
|
/* Use a plain object with keys re and im for a complex; note the components
|
||||||
typed.addType({
|
* can be any type (for this proof-of-concept; in reality we'd want to
|
||||||
name: 'Complex',
|
* insist on some numeric or scalar supertype).
|
||||||
test: z => z && typeof z === 'object' && 're' in z && 'im' in z
|
*/
|
||||||
})
|
export function isComplex(z) {
|
||||||
|
return z && typeof z === 'object' && 're' in z && 'im' in z
|
||||||
|
}
|
||||||
|
|
||||||
|
typed.addType({name: 'Complex', test: isComplex})
|
||||||
typed.addConversion({
|
typed.addConversion({
|
||||||
from: 'number',
|
from: 'number',
|
||||||
to: 'Complex',
|
to: 'Complex',
|
||||||
convert: x => ({re: x, im: 0})
|
convert: x => ({re: x, im: 0})
|
||||||
})
|
})
|
||||||
|
/* Pleasantly enough, it is OK to add this conversion even if there is no
|
||||||
|
* type 'bigint' defined, so everything should Just Work.
|
||||||
|
*/
|
||||||
|
typed.addConversion({
|
||||||
|
from: 'bigint',
|
||||||
|
to: 'Complex',
|
||||||
|
convert: 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'
|
||||||
|
}
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
import './Complex.mjs'
|
import {numComplex} from './Complex.mjs'
|
||||||
|
|
||||||
export const add = {
|
export const add = {
|
||||||
'...Complex': [[], addends => {
|
'...Complex': [['self'], ref => addends => {
|
||||||
const sum = {re: 0, im: 0}
|
if (addends.length === 0) return {re:0, im:0}
|
||||||
addends.forEach(addend => {
|
const seed = addends.shift()
|
||||||
sum.re += addend.re
|
return addends.reduce((w,z) => {
|
||||||
sum.im += addend.im
|
/* Need a "base case" to avoid infinite self-reference loops */
|
||||||
})
|
if (numComplex(z) && numComplex(w)) {
|
||||||
return sum
|
return {re: w.re + z.re, im: w.im + z.im}
|
||||||
|
}
|
||||||
|
return {re: ref.self(w.re, z.re), im: ref.self(w.im, z.im)}
|
||||||
|
}, seed)
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
@ -1,2 +1,4 @@
|
|||||||
export {complex} from './complex.mjs'
|
export {complex} from './complex.mjs'
|
||||||
export {add} from './add.mjs'
|
export {add} from './add.mjs'
|
||||||
|
export {negate} from './negate.mjs'
|
||||||
|
export {subtract} from '../generic/subtract.mjs'
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
import './Complex.mjs'
|
import './Complex.mjs'
|
||||||
|
|
||||||
export const complex = {
|
export const complex = {
|
||||||
'number, number': [[], (x, y) => ({re: x, im: y})],
|
/* 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]
|
Complex: [[], z => z]
|
||||||
}
|
}
|
||||||
|
8
complex/negate.mjs
Normal file
8
complex/negate.mjs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import {numComplex} from './Complex.mjs'
|
||||||
|
export const negate = {
|
||||||
|
/* need a "base case" to avoid infinite self-reference */
|
||||||
|
Complex: [['self'], ref => z => {
|
||||||
|
if (numComplex(z)) return {re: -z.re, im: -z.im}
|
||||||
|
return {re: ref.self(z.re), im: ref.self(z.im)}
|
||||||
|
}]
|
||||||
|
}
|
@ -1,10 +1,12 @@
|
|||||||
/* Core of pocomath: generates the default instance */
|
/* Core of pocomath: generates the default instance */
|
||||||
import PocomathInstance from './PocomathInstance.mjs'
|
import PocomathInstance from './PocomathInstance.mjs'
|
||||||
import * as numbers from './number/all.mjs'
|
import * as numbers from './number/all.mjs'
|
||||||
|
import * as bigints from './bigint/all.mjs'
|
||||||
import * as complex from './complex/all.mjs'
|
import * as complex from './complex/all.mjs'
|
||||||
|
|
||||||
const math = new PocomathInstance('math')
|
const math = new PocomathInstance('math')
|
||||||
math.install(numbers)
|
math.install(numbers)
|
||||||
|
math.install(bigints)
|
||||||
math.install(complex)
|
math.install(complex)
|
||||||
|
|
||||||
export default math
|
export default math
|
||||||
|
@ -29,5 +29,27 @@ describe('The default full pocomath instance "math"', () => {
|
|||||||
assert.deepStrictEqual(math.complex(2,3), norm13)
|
assert.deepStrictEqual(math.complex(2,3), norm13)
|
||||||
assert.deepStrictEqual(math.complex(2), math.complex(2,0))
|
assert.deepStrictEqual(math.complex(2), math.complex(2,0))
|
||||||
assert.deepStrictEqual(math.add(2, math.complex(0,3)), norm13)
|
assert.deepStrictEqual(math.add(2, math.complex(0,3)), norm13)
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
math.subtract(16, math.add(3, math.complex(0,4), 2)),
|
||||||
|
math.complex(11, -4))
|
||||||
|
assert.strictEqual(math.negate(math.complex(3, 8)).im, -8)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles bigints', () => {
|
||||||
|
assert.strictEqual(math.negate(5n), -5n)
|
||||||
|
assert.strictEqual(math.subtract(12n, 5n), 7n)
|
||||||
|
assert.strictEqual(math.add(15n, 25n, 35n), 75n)
|
||||||
|
assert.strictEqual(math.add(10n, math.negate(3n)), 7n)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles Gaussian integers', () => {
|
||||||
|
const norm13n = {re: 2n, im: 3n}
|
||||||
|
assert.deepStrictEqual(math.complex(2n,3n), norm13n)
|
||||||
|
assert.deepStrictEqual(math.complex(2n), math.complex(2n, 0n))
|
||||||
|
assert.deepStrictEqual(math.add(2n, math.complex(0n, 3n)), norm13n)
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
math.subtract(16n, math.add(3n, math.complex(0n,4n), 2n)),
|
||||||
|
math.complex(11n, -4n))
|
||||||
|
assert.strictEqual(math.negate(math.complex(3n, 8n)).im, -8n)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,14 +1,19 @@
|
|||||||
import assert from 'assert'
|
import assert from 'assert'
|
||||||
|
import math from '../pocomath.mjs'
|
||||||
import typed from 'typed-function'
|
import typed from 'typed-function'
|
||||||
import PocomathInstance from '../PocomathInstance.mjs'
|
import PocomathInstance from '../PocomathInstance.mjs'
|
||||||
import * as numbers from '../number/all.mjs'
|
import * as numbers from '../number/all.mjs'
|
||||||
import * as complex from '../complex/all.mjs'
|
import * as complex from '../complex/all.mjs'
|
||||||
|
import * as complexAdd from '../complex/add.mjs'
|
||||||
|
import * as complexNegate from '../complex/negate.mjs'
|
||||||
|
|
||||||
const bw = new PocomathInstance('backwards')
|
const bw = new PocomathInstance('backwards')
|
||||||
describe('A custom instance', () => {
|
describe('A custom instance', () => {
|
||||||
it("works when partially assembled", () => {
|
it("works when partially assembled", () => {
|
||||||
bw.install(complex)
|
bw.install(complex)
|
||||||
assert.deepStrictEqual(bw.add(2, bw.complex(0, 3)), {re: 2, im: 3})
|
assert.deepStrictEqual(bw.add(2, bw.complex(0, 3)), {re: 2, im: 3})
|
||||||
|
assert.deepStrictEqual(bw.negate(2), bw.complex(-2,-0))
|
||||||
|
assert.deepStrictEqual(bw.subtract(2, bw.complex(0, 3)), {re: 2, im: -3})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("can be assembled in any order", () => {
|
it("can be assembled in any order", () => {
|
||||||
@ -17,5 +22,20 @@ describe('A custom instance', () => {
|
|||||||
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})
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
bw.subtract(16, bw.add(3, bw.complex(0,4), 2)),
|
||||||
|
math.complex(11, -4)) // note both instances coexist
|
||||||
|
assert.deepStrictEqual(bw.negate(math.complex(3, '8')).im, -8)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("can be assembled piecemeal", () => {
|
||||||
|
const pm = new PocomathInstance('piecemeal')
|
||||||
|
pm.install(numbers)
|
||||||
|
assert.strictEqual(pm.subtract(5, 10), -5)
|
||||||
|
pm.install(complexAdd)
|
||||||
|
pm.install(complexNegate)
|
||||||
|
// Should be enough to allow complex subtraction, as subtract is generic
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
pm.subtract({re:5, im:0}, {re:10, im:1}), {re:-5, im: -1})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user