diff --git a/PocomathInstance.mjs b/PocomathInstance.mjs index 3ca4385..facdfb4 100644 --- a/PocomathInstance.mjs +++ b/PocomathInstance.mjs @@ -5,6 +5,7 @@ export default class PocomathInstance { constructor(name) { this.name = name this._imps = {} + this._affects = {} } /** @@ -41,11 +42,19 @@ export default class PocomathInstance { const opImps = this._imps[name] for (const signature in implementations) { if (signature in opImps) { - if (implemenatations[signature] === opImps[signature]) continue + 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) + } } } } @@ -62,6 +71,11 @@ export default class PocomathInstance { if (!(name in this._imps)) { 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 } else { const refs = {} + let self_referential = false 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 if (dep.includes('(')) { 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) diff --git a/bigint/BigInt.mjs b/bigint/BigInt.mjs new file mode 100644 index 0000000..c1d60d1 --- /dev/null +++ b/bigint/BigInt.mjs @@ -0,0 +1,3 @@ +import typed from 'typed-function' + +typed.addType({name: 'bigint', test: b => typeof b === 'bigint'}) diff --git a/bigint/add.mjs b/bigint/add.mjs new file mode 100644 index 0000000..6bb1282 --- /dev/null +++ b/bigint/add.mjs @@ -0,0 +1,4 @@ +import './BigInt.mjs' +export const add = { + '...bigint': [[], addends => addends.reduce((x,y) => x+y, 0n)], +} diff --git a/bigint/all.mjs b/bigint/all.mjs new file mode 100644 index 0000000..395eb54 --- /dev/null +++ b/bigint/all.mjs @@ -0,0 +1,3 @@ +export {add} from './add.mjs' +export {negate} from './negate.mjs' +export {subtract} from '../generic/subtract.mjs' diff --git a/bigint/negate.mjs b/bigint/negate.mjs new file mode 100644 index 0000000..ec89bac --- /dev/null +++ b/bigint/negate.mjs @@ -0,0 +1,2 @@ +import './BigInt.mjs' +export const negate = {bigint: [[], b => -b ]} diff --git a/complex/Complex.mjs b/complex/Complex.mjs index b4b34fe..86879fb 100644 --- a/complex/Complex.mjs +++ b/complex/Complex.mjs @@ -1,14 +1,29 @@ import typed from 'typed-function' -/* Use a plain object with keys re and im for a complex */ -typed.addType({ - name: 'Complex', - test: z => z && typeof z === 'object' && 're' in z && 'im' in z -}) +/* 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 +} +typed.addType({name: 'Complex', test: isComplex}) typed.addConversion({ from: 'number', to: 'Complex', 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, so to speak: */ +export function numComplex(z) { + return isComplex(z) && typeof z.re === 'number' && typeof z.im === 'number' +} diff --git a/complex/add.mjs b/complex/add.mjs index cefc410..f43c883 100644 --- a/complex/add.mjs +++ b/complex/add.mjs @@ -1,12 +1,15 @@ -import './Complex.mjs' +import {numComplex} from './Complex.mjs' export const add = { - '...Complex': [[], addends => { - const sum = {re: 0, im: 0} - addends.forEach(addend => { - sum.re += addend.re - sum.im += addend.im - }) - return sum + '...Complex': [['self'], ref => addends => { + if (addends.length === 0) return {re:0, im:0} + const seed = addends.shift() + return addends.reduce((w,z) => { + /* Need a "base case" to avoid infinite self-reference loops */ + if (numComplex(z) && numComplex(w)) { + 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) }] } diff --git a/complex/all.mjs b/complex/all.mjs index 6f7e3d0..ebd9375 100644 --- a/complex/all.mjs +++ b/complex/all.mjs @@ -1,2 +1,4 @@ export {complex} from './complex.mjs' export {add} from './add.mjs' +export {negate} from './negate.mjs' +export {subtract} from '../generic/subtract.mjs' diff --git a/complex/complex.mjs b/complex/complex.mjs index 4404b80..c8941fc 100644 --- a/complex/complex.mjs +++ b/complex/complex.mjs @@ -1,6 +1,11 @@ import './Complex.mjs' 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] } diff --git a/complex/negate.mjs b/complex/negate.mjs new file mode 100644 index 0000000..429a575 --- /dev/null +++ b/complex/negate.mjs @@ -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)} + }] +} diff --git a/pocomath.mjs b/pocomath.mjs index c58ef45..5a9bd4a 100644 --- a/pocomath.mjs +++ b/pocomath.mjs @@ -1,10 +1,12 @@ /* Core of pocomath: generates the default instance */ import PocomathInstance from './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 diff --git a/test/_pocomath.mjs b/test/_pocomath.mjs index b816844..af24836 100644 --- a/test/_pocomath.mjs +++ b/test/_pocomath.mjs @@ -29,5 +29,27 @@ describe('The default full pocomath instance "math"', () => { assert.deepStrictEqual(math.complex(2,3), norm13) assert.deepStrictEqual(math.complex(2), math.complex(2,0)) 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) }) }) diff --git a/test/custom.mjs b/test/custom.mjs index ed8d7cc..78dd8db 100644 --- a/test/custom.mjs +++ b/test/custom.mjs @@ -1,14 +1,19 @@ import assert from 'assert' +import math from '../pocomath.mjs' import typed from 'typed-function' import PocomathInstance from '../PocomathInstance.mjs' import * as numbers from '../number/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') describe('A custom instance', () => { it("works when partially assembled", () => { bw.install(complex) 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", () => { @@ -17,5 +22,20 @@ describe('A custom instance', () => { 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}) + 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}) }) })