diff --git a/complex/add.js b/complex/add.js index 1293a14..a831574 100644 --- a/complex/add.js +++ b/complex/add.js @@ -2,6 +2,7 @@ import { anyComplex } from './complex.js' export default function create(pmath) { const complex = pmath('complex') + const resolve = pmath.config.resolveComplex return pmath('add', [anyComplex, // naive, but this is just a P-o-C (...addends) => { let sum = complex(addends[0]) @@ -10,7 +11,9 @@ export default function create(pmath) { sum.re += addend.re sum.im += addend.im } + if (resolve && Math.abs(sum.im/sum.re) < 1e-10) return sum.re return sum - }]) + }], + create, pmath) // register ourselves for invalidation and reconfiguration } diff --git a/generic/subtract.js b/generic/subtract.js index 236885c..87745e0 100644 --- a/generic/subtract.js +++ b/generic/subtract.js @@ -1,9 +1,9 @@ export default function create(pmath) { const add = pmath('add') const negate = pmath('negate') - if (!pmath.subtract) { // avoid double definition at cost of extensibility - pmath('subtract', [args => args.length === 2, - (x, y) => add(x, negate(y))]) - } + return pmath( + 'subtract', + [args => args.length === 2, (x, y) => add(x, negate(y))], + create, pmath) return pmath.subtract } diff --git a/picomathInstance.js b/picomathInstance.js index 04afd13..90e4041 100644 --- a/picomathInstance.js +++ b/picomathInstance.js @@ -1,23 +1,44 @@ /* Core of picomath: generates an instance */ import poortf from './poortf.js' -export default function picomathInstance (instName) { - /* Since we have to do it all the time, when we call a picomath instance - * as a function, it takes a name and 0 or more implementations add adds - * them to its poortf property named name, returning that property value. - */ - function fn (name, imps) { - if (name in fn) { - fn[name].addImps(imps) - } else { - fn[name] = poortf(name, imps) - } - return fn[name] - } +export default function picomathInstance (instName, config) { + /* Since we have to do it all the time, when we call a picomath instance + * as a function, it takes a name and 0 or more implementations add adds + * them to its poortf property named name, returning that property value. + */ + function fn (name, imps, author, data) { + if (name in fn) { + fn[name].addImps(imps, author, data) + } else { + fn[name] = poortf(name, imps, author, data) + } + /* For PoC, just assume only reason to provide author info + * is to be invalidated when config changes + */ + if (author) { + if (!fn.configUsers.has(author)) { + fn.configUsers.set(author, new Set()) + } + fn.configUsers.get(author).add(name) + } + return fn[name] + } - Object.defineProperty(fn, 'name', {value: instName}) - - return fn + Object.defineProperty(fn, 'name', {value: instName}) + // There is an issue below of possible collision between the following + // property names and operation names. Since it would not be too hard to + // solve, we won't worry about it in this PoC. + fn.config = config || { resolveComplex: false } // primitive default for POC + fn.configUsers = new Map() + fn.reconfigure = config => { + for (const [author, names] of fn.configUsers.entries()) { + for (const name of names) { + fn[name].invalidate(author) + } + } + fn.config = config + } + return fn } diff --git a/poortf.js b/poortf.js index 0b212d7..68936ab 100644 --- a/poortf.js +++ b/poortf.js @@ -1,13 +1,33 @@ /* Totally minimal "typed functions" */ -const addImps = (dest, imps) => { +/* helper: dest is really a TF and imps is either nothing, an imp, or + * an array of implementations. author is the entity responsible for these + * implementations. + */ +const addImps = (dest, imps, author) => { if (imps) { if (!Array.isArray(imps[0])) imps = [imps] - for (const imp of imps) dest.push(imp) + } else { + imps = [] } + if (author) { + if (dest.authors.has(author)) { + const [count, index] = dest.authors.get(author) + if (count) dest.imps.splice(index, count) + } + dest.authors.set(author, [imps.length, dest.imps.length]) + } + for (const imp of imps) dest.imps.push(imp) } -export default function poortf (name, imps) { +/* Create a TF, optionally adding some initial imps and further optionally + * registering them to an author. There are two advantages to author + * registration: (1) if the same author adds implementations, the prior imps + * will be deleted, and (2) the author can be invalidated, and the TF will + * lazily call the author back with the given data the next time it is + * called, so that it can re-add the imps. + */ +export default function poortf (name, imps, author, data) { /* This is the (function) object we will return */ function fn () { for (const imp of fn.imps) { @@ -20,9 +40,40 @@ export default function poortf (name, imps) { /* Now dress it up for use */ Object.defineProperty(fn, 'name', {value: name}) fn.imps = [] - addImps(fn.imps, imps) - fn.addImps = newI => addImps(fn.imps, newI) - + fn.authors = new Map() // tracks who made each implementation + fn.authorData = new Map() // tracks author callback data + addImps(fn, imps, author) + if (author) fn.authorData.set(author, data) + fn.addImps = (newI, author, data) => { + addImps(fn, newI, author) + if (author) fn.authorData.set(author, data) + } + fn.invalidate = author => { + addImps(fn, null, author) // deletes the author's imps + fn.unshiftImp([() => true, (...args) => fn.reloadAndRecall(args)]) + } + fn.unshiftImp = newImp => { + fn.imps.unshift(newImp) + for (const record in fn.authors.values()) { + record[1] += 1 + } + } + fn.shiftImp = () => { + const oldImp = fn.imps.shift() + for (const record in fn.authors.values()) { + record[1] -= 1 + } + } + fn.reloadAndRecall = (argArray) => { + fn.shiftImp() // throw ourself away, ha ha + for (const author of fn.authorData.keys()) { + if (fn.authors.get(author)[0] === 0) { + // imps count of 0 means the author was invalidated + author(fn.authorData.get(author)) + } + } + return fn.apply(null, argArray) + } return fn } diff --git a/test/_picomath.js b/test/_picomath.js index 84ff19b..d0126e7 100644 --- a/test/_picomath.js +++ b/test/_picomath.js @@ -20,4 +20,8 @@ describe('The default full picomath instance "math"', () => { math.complex(11, -4)) assert.deepStrictEqual(math.negate(math.complex(3, '8')).im, -8) }) + + it('does not double-define subtract', () => { + assert.deepStrictEqual(math.subtract.imps.length, 1) + }) }) diff --git a/test/_poortf.js b/test/_poortf.js index 3984407..a5f9f6f 100644 --- a/test/_poortf.js +++ b/test/_poortf.js @@ -21,13 +21,22 @@ describe('poortf', () => { assert.throws(() => add('kilroy'), TypeError) }) + const defNumber = what => { + slate.addImps([args => typeof args[0] === 'number', + n => 'I am not number ' + (what+n)], + defNumber, what+1) + } + it('extends an empty tf', () => { + defNumber(2) + assert.strictEqual(slate(0, 'was here'), 'I am not number 2') + assert.throws(() => slate('Kilroy', 'was here'), TypeError) slate.addImps([ [args => typeof args[0] === 'string', s => s + ' wuz here'], - [args => typeof args[0] === 'number', () => 'I am not a number'] + [args => typeof args[0] === 'boolean', () => 'Maybe I am a number'] ]) assert.strictEqual(slate('Kilroy', 'was here'), 'Kilroy wuz here') - assert.strictEqual(slate(2, 'was here'), 'I am not a number') + assert.strictEqual(slate(true), 'Maybe I am a number') assert.throws(() => slate(['Ha!']), TypeError) }) @@ -36,6 +45,15 @@ describe('poortf', () => { assert.strictEqual(add('Kilroy', 23), 'Kilroy23') assert.throws(() => add(['Ha!'], 'gotcha'), TypeError) }) + + it('can invalidate and lazily reload implementations', () => { + slate.invalidate(defNumber) + assert.strictEqual(slate.imps.length, 3) // reloader plus two non-number + assert.strictEqual(slate(1), 'I am not number 4') // 'what' is now 3!! + assert.strictEqual(slate('Yorlik', 7), 'Yorlik wuz here') + assert.strictEqual(slate(false), 'Maybe I am a number') + assert.strictEqual(slate.imps.length, 3) // reloader gone + }) }) diff --git a/test/custom.js b/test/custom.js index c14e849..1107800 100644 --- a/test/custom.js +++ b/test/custom.js @@ -36,4 +36,20 @@ describe('Custom instances', () => { assert.strictEqual('subtract' in cherry, false) assert.strictEqual('negate' in cherry, false) }) + + const res = picoInstance('resolving', { resolveComplex: true }) + createNumbers(res) + createComplex(res) + + it("can be configured", () => { + assert.strictEqual(res.add(res.complex(2,3), res.complex(2,-3)), 4) + assert.deepStrictEqual(math.add(math.complex(2,3), math.complex(2,-3)), + math.complex(4,0)) + }) + + it("can be reconfigured", () => { + res.reconfigure({resolveComplex: false}) + assert.deepStrictEqual(math.add(math.complex(2,3), math.complex(2,-3)), + math.complex(4,0)) + }) })