From 00a7f79552694b1b2ba250fa2cd668c0c00095af Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Fri, 25 Mar 2022 11:31:06 -0700 Subject: [PATCH 1/4] feat(poortf): add concept of 'author' to an imp of a TF And make it so that if the same author adds imps again, they replace the previous imps from that author. Use this feature to avoid the explicit check for double-inclusion in subtract, and test it worked. --- generic/subtract.js | 8 ++++---- picomathInstance.js | 6 +++--- poortf.js | 29 +++++++++++++++++++++++------ test/_picomath.js | 4 ++++ 4 files changed, 34 insertions(+), 13 deletions(-) diff --git a/generic/subtract.js b/generic/subtract.js index 236885c..c64c15b 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) return pmath.subtract } diff --git a/picomathInstance.js b/picomathInstance.js index 04afd13..0eed730 100644 --- a/picomathInstance.js +++ b/picomathInstance.js @@ -6,11 +6,11 @@ export default function picomathInstance (instName) { * 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) { + function fn (name, imps, author) { if (name in fn) { - fn[name].addImps(imps) + fn[name].addImps(imps, author) } else { - fn[name] = poortf(name, imps) + fn[name] = poortf(name, imps, author) } return fn[name] } diff --git a/poortf.js b/poortf.js index 0b212d7..afbb9dc 100644 --- a/poortf.js +++ b/poortf.js @@ -1,13 +1,29 @@ /* 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) + 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 the next time it is called to re-add the imps. + */ +export default function poortf (name, imps, author) { /* This is the (function) object we will return */ function fn () { for (const imp of fn.imps) { @@ -20,8 +36,9 @@ 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 + addImps(fn, imps, author) + fn.addImps = (newI, author) => addImps(fn, newI, author) 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) + }) }) From 1c1ba91e481d91428ccede7ec939161bde0c0328 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Fri, 25 Mar 2022 13:09:50 -0700 Subject: [PATCH 2/4] feat: Invalidation and lazy reloading of implementations The interface is a bit clunky, the "author" and the callback "data" have to be specified when adding (an) implementation(s) in order for this to work. But it does, and this is just a PoC, the interface could be cleaned up. --- generic/subtract.js | 2 +- picomathInstance.js | 6 ++--- poortf.js | 58 +++++++++++++++++++++++++++++++++++---------- test/_poortf.js | 22 +++++++++++++++-- 4 files changed, 70 insertions(+), 18 deletions(-) diff --git a/generic/subtract.js b/generic/subtract.js index c64c15b..87745e0 100644 --- a/generic/subtract.js +++ b/generic/subtract.js @@ -4,6 +4,6 @@ export default function create(pmath) { return pmath( 'subtract', [args => args.length === 2, (x, y) => add(x, negate(y))], - create) + create, pmath) return pmath.subtract } diff --git a/picomathInstance.js b/picomathInstance.js index 0eed730..8bac098 100644 --- a/picomathInstance.js +++ b/picomathInstance.js @@ -6,11 +6,11 @@ export default function picomathInstance (instName) { * 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) { + function fn (name, imps, author, data) { if (name in fn) { - fn[name].addImps(imps, author) + fn[name].addImps(imps, author, data) } else { - fn[name] = poortf(name, imps, author) + fn[name] = poortf(name, imps, author, data) } return fn[name] } diff --git a/poortf.js b/poortf.js index afbb9dc..68936ab 100644 --- a/poortf.js +++ b/poortf.js @@ -7,23 +7,27 @@ const addImps = (dest, imps, author) => { if (imps) { if (!Array.isArray(imps[0])) imps = [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) + } 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) } + /* 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 the next time it is called to re-add the imps. + * 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) { +export default function poortf (name, imps, author, data) { /* This is the (function) object we will return */ function fn () { for (const imp of fn.imps) { @@ -37,9 +41,39 @@ export default function poortf (name, imps, author) { Object.defineProperty(fn, 'name', {value: name}) fn.imps = [] fn.authors = new Map() // tracks who made each implementation + fn.authorData = new Map() // tracks author callback data addImps(fn, imps, author) - fn.addImps = (newI, author) => addImps(fn, newI, 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/_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 + }) }) From 4213ade4ba5bd010e497eac48f39bf254361e165 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Fri, 25 Mar 2022 13:43:37 -0700 Subject: [PATCH 3/4] feat: Add a configuration for an instance that can be set at creation time And use it to control whether the result of a complex operation where the imaginary part comes out to 0 will be coerced back to a number. --- complex/add.js | 2 ++ picomathInstance.js | 3 ++- test/custom.js | 10 ++++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/complex/add.js b/complex/add.js index 1293a14..fc07a9c 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,6 +11,7 @@ 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 }]) } diff --git a/picomathInstance.js b/picomathInstance.js index 8bac098..759d215 100644 --- a/picomathInstance.js +++ b/picomathInstance.js @@ -1,7 +1,7 @@ /* Core of picomath: generates an instance */ import poortf from './poortf.js' -export default function picomathInstance (instName) { +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. @@ -16,6 +16,7 @@ export default function picomathInstance (instName) { } Object.defineProperty(fn, 'name', {value: instName}) + fn.config = config || { resolveComplex: false } // primitive default for POC return fn } diff --git a/test/custom.js b/test/custom.js index c14e849..69ac993 100644 --- a/test/custom.js +++ b/test/custom.js @@ -36,4 +36,14 @@ 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)) + }) }) From d95e5ad930e16ff535377a28a3ca428840b92779 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Fri, 25 Mar 2022 14:40:57 -0700 Subject: [PATCH 4/4] feat: Allow reconfiguration of an existing picomath instance Keeps track of which operations depend on the configuration and invalidates them for lazy reconfiguration when the config changes. --- complex/add.js | 3 ++- picomathInstance.js | 52 +++++++++++++++++++++++++++++++-------------- test/custom.js | 6 ++++++ 3 files changed, 44 insertions(+), 17 deletions(-) diff --git a/complex/add.js b/complex/add.js index fc07a9c..a831574 100644 --- a/complex/add.js +++ b/complex/add.js @@ -13,6 +13,7 @@ export default function create(pmath) { } 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/picomathInstance.js b/picomathInstance.js index 759d215..90e4041 100644 --- a/picomathInstance.js +++ b/picomathInstance.js @@ -2,23 +2,43 @@ import poortf from './poortf.js' 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) - } - return fn[name] - } + /* 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}) - fn.config = config || { resolveComplex: false } // primitive default for POC - - 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/test/custom.js b/test/custom.js index 69ac993..1107800 100644 --- a/test/custom.js +++ b/test/custom.js @@ -46,4 +46,10 @@ describe('Custom instances', () => { 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)) + }) })