From 0069597a76e2c22a4f1e464609af6ef98c0d390c Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Fri, 22 Jul 2022 20:49:14 +0000 Subject: [PATCH] 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 Reviewed-on: https://code.studioinfinity.org/glen/pocomath/pulls/15 --- bigint/BigInt.mjs | 3 - complex/add.mjs | 15 ----- complex/negate.mjs | 8 --- number/negate.mjs | 3 - package.json5 | 4 +- src/bigint/Types/bigint.mjs | 3 + {bigint => src/bigint}/add.mjs | 3 +- {number => src/bigint}/all.mjs | 1 + {bigint => src/bigint}/negate.mjs | 3 +- .../types => src/complex/Types}/Complex.mjs | 23 +++----- src/complex/add.mjs | 10 ++++ {complex => src/complex}/all.mjs | 1 + {complex => src/complex}/complex.mjs | 2 +- {complex => src/complex}/extendToComplex.mjs | 1 - src/complex/negate.mjs | 7 +++ .../core/PocomathInstance.mjs | 55 +++++++++++++++++-- {generic => src/generic}/subtract.mjs | 0 src/number/Types/number.mjs | 7 +++ {number => src/number}/add.mjs | 2 + {bigint => src/number}/all.mjs | 1 + src/number/negate.mjs | 3 + pocomath.mjs => src/pocomath.mjs | 2 +- test/_pocomath.mjs | 2 +- test/{ => core}/_PocomathInstance.mjs | 2 +- test/custom.mjs | 33 +++++------ 25 files changed, 120 insertions(+), 74 deletions(-) delete mode 100644 bigint/BigInt.mjs delete mode 100644 complex/add.mjs delete mode 100644 complex/negate.mjs delete mode 100644 number/negate.mjs create mode 100644 src/bigint/Types/bigint.mjs rename {bigint => src/bigint}/add.mjs (68%) rename {number => src/bigint}/all.mjs (73%) rename {bigint => src/bigint}/negate.mjs (52%) rename {complex/types => src/complex/Types}/Complex.mjs (53%) create mode 100644 src/complex/add.mjs rename {complex => src/complex}/all.mjs (78%) rename {complex => src/complex}/complex.mjs (88%) rename {complex => src/complex}/extendToComplex.mjs (94%) create mode 100644 src/complex/negate.mjs rename PocomathInstance.mjs => src/core/PocomathInstance.mjs (68%) rename {generic => src/generic}/subtract.mjs (100%) create mode 100644 src/number/Types/number.mjs rename {number => src/number}/add.mjs (67%) rename {bigint => src/number}/all.mjs (73%) create mode 100644 src/number/negate.mjs rename pocomath.mjs => src/pocomath.mjs (84%) rename test/{ => core}/_PocomathInstance.mjs (87%) diff --git a/bigint/BigInt.mjs b/bigint/BigInt.mjs deleted file mode 100644 index c1d60d1..0000000 --- a/bigint/BigInt.mjs +++ /dev/null @@ -1,3 +0,0 @@ -import typed from 'typed-function' - -typed.addType({name: 'bigint', test: b => typeof b === 'bigint'}) diff --git a/complex/add.mjs b/complex/add.mjs deleted file mode 100644 index 9cefa0a..0000000 --- a/complex/add.mjs +++ /dev/null @@ -1,15 +0,0 @@ -import {numComplex} 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) => { - /* 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/negate.mjs b/complex/negate.mjs deleted file mode 100644 index 77dfb01..0000000 --- a/complex/negate.mjs +++ /dev/null @@ -1,8 +0,0 @@ -import {numComplex} from './types/Complex.mjs' -export const negate = { - Complex: [['self'], ref => z => { - /* need a "base case" to avoid infinite self-reference */ - if (numComplex(z)) return {re: -z.re, im: -z.im} - return {re: ref.self(z.re), im: ref.self(z.im)} - }] -} diff --git a/number/negate.mjs b/number/negate.mjs deleted file mode 100644 index b9a73cb..0000000 --- a/number/negate.mjs +++ /dev/null @@ -1,3 +0,0 @@ -export const negate = { - number: [[], n => -n] -} diff --git a/package.json5 b/package.json5 index 6c804ce..f83ee88 100644 --- a/package.json5 +++ b/package.json5 @@ -5,7 +5,9 @@ inclusion, avoiding factory functions.', main: 'index.js', scripts: { - test: '!(find . | sort -f | uniq -i -c | grep -v " 1 ") && npx mocha', + 'test:filecase': '!(find . | sort -f | uniq -i -c | grep -v " 1 ")', + 'test:unit': 'npx mocha --recursive', + test: 'pnpm test:filecase && pnpm test:unit', }, repository: { type: 'git', diff --git a/src/bigint/Types/bigint.mjs b/src/bigint/Types/bigint.mjs new file mode 100644 index 0000000..45ece8f --- /dev/null +++ b/src/bigint/Types/bigint.mjs @@ -0,0 +1,3 @@ +export const Types = { + bigint: {test: b => typeof b === 'bigint'} +} diff --git a/bigint/add.mjs b/src/bigint/add.mjs similarity index 68% rename from bigint/add.mjs rename to src/bigint/add.mjs index 6bb1282..d6fc0c7 100644 --- a/bigint/add.mjs +++ b/src/bigint/add.mjs @@ -1,4 +1,5 @@ -import './BigInt.mjs' +export {Types} from './Types/bigint.mjs' + export const add = { '...bigint': [[], addends => addends.reduce((x,y) => x+y, 0n)], } diff --git a/number/all.mjs b/src/bigint/all.mjs similarity index 73% rename from number/all.mjs rename to src/bigint/all.mjs index 395eb54..f4cdb60 100644 --- a/number/all.mjs +++ b/src/bigint/all.mjs @@ -1,3 +1,4 @@ +export {Types} from './Types/bigint.mjs' export {add} from './add.mjs' export {negate} from './negate.mjs' export {subtract} from '../generic/subtract.mjs' diff --git a/bigint/negate.mjs b/src/bigint/negate.mjs similarity index 52% rename from bigint/negate.mjs rename to src/bigint/negate.mjs index ec89bac..538e1d4 100644 --- a/bigint/negate.mjs +++ b/src/bigint/negate.mjs @@ -1,2 +1,3 @@ -import './BigInt.mjs' +export {Types} from './Types/bigint.mjs' + export const negate = {bigint: [[], b => -b ]} diff --git a/complex/types/Complex.mjs b/src/complex/Types/Complex.mjs similarity index 53% rename from complex/types/Complex.mjs rename to src/complex/Types/Complex.mjs index 86879fb..e3f29e1 100644 --- a/complex/types/Complex.mjs +++ b/src/complex/Types/Complex.mjs @@ -1,5 +1,3 @@ -import typed from 'typed-function' - /* 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). @@ -8,20 +6,13 @@ 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}) -}) +export const Types = { + Complex: { + test: isComplex, + number: x => ({re: x, im: 0}), + bigint: x => ({re: x, im: 0n}) + } +} /* test if an entity is Complex, so to speak: */ export function numComplex(z) { diff --git a/src/complex/add.mjs b/src/complex/add.mjs new file mode 100644 index 0000000..3583b38 --- /dev/null +++ b/src/complex/add.mjs @@ -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) + }] +} diff --git a/complex/all.mjs b/src/complex/all.mjs similarity index 78% rename from complex/all.mjs rename to src/complex/all.mjs index ebd9375..d5d5bda 100644 --- a/complex/all.mjs +++ b/src/complex/all.mjs @@ -1,3 +1,4 @@ +export {Types} from './Types/Complex.mjs' export {complex} from './complex.mjs' export {add} from './add.mjs' export {negate} from './negate.mjs' diff --git a/complex/complex.mjs b/src/complex/complex.mjs similarity index 88% rename from complex/complex.mjs rename to src/complex/complex.mjs index 9d7f0c5..c86c3e8 100644 --- a/complex/complex.mjs +++ b/src/complex/complex.mjs @@ -1,4 +1,4 @@ -import './types/Complex.mjs' +export {Types} from './Types/Complex.mjs' export const complex = { /* Very permissive for sake of proof-of-concept; would be better to diff --git a/complex/extendToComplex.mjs b/src/complex/extendToComplex.mjs similarity index 94% rename from complex/extendToComplex.mjs rename to src/complex/extendToComplex.mjs index 64f4736..21e1a29 100644 --- a/complex/extendToComplex.mjs +++ b/src/complex/extendToComplex.mjs @@ -1,4 +1,3 @@ -import './types/Complex.mjs' import * as complex from './complex.mjs' /* Add all the complex implementations for functions already diff --git a/src/complex/negate.mjs b/src/complex/negate.mjs new file mode 100644 index 0000000..bf4e612 --- /dev/null +++ b/src/complex/negate.mjs @@ -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)} + }] +} diff --git a/PocomathInstance.mjs b/src/core/PocomathInstance.mjs similarity index 68% rename from PocomathInstance.mjs rename to src/core/PocomathInstance.mjs index facdfb4..307f832 100644 --- a/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -6,6 +6,10 @@ export default class PocomathInstance { 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'}]) } /** @@ -29,7 +33,14 @@ export default class PocomathInstance { * 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 + * 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]) @@ -47,7 +58,7 @@ export default class PocomathInstance { `Conflicting definitions of ${signature} for ${name}`) } else { opImps[signature] = implementations[signature] - for (const dep of implementations[signature][0]) { + for (const dep of implementations[signature][0] || []) { const depname = dep.split('(', 1)[0] if (depname === 'self') continue if (!(depname in this._affects)) { @@ -87,6 +98,7 @@ export default class PocomathInstance { 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] @@ -107,7 +119,7 @@ export default class PocomathInstance { } } if (self_referential) { - tf_imps[signature] = typed.referToSelf(self => { + tf_imps[signature] = this._typed.referToSelf(self => { refs.self = self return imp(refs) }) @@ -116,7 +128,7 @@ export default class PocomathInstance { } } } - const tf = typed(name, tf_imps) + const tf = this._typed(name, tf_imps) this[name] = tf return tf } @@ -127,7 +139,40 @@ export default class PocomathInstance { */ _ensureBundle(name) { const maybe = this[name] - if (typed.isTypedFunction(maybe)) return maybe + 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) + } + } diff --git a/generic/subtract.mjs b/src/generic/subtract.mjs similarity index 100% rename from generic/subtract.mjs rename to src/generic/subtract.mjs diff --git a/src/number/Types/number.mjs b/src/number/Types/number.mjs new file mode 100644 index 0000000..40e390b --- /dev/null +++ b/src/number/Types/number.mjs @@ -0,0 +1,7 @@ +export const Types = { + number: { + test: n => typeof n === 'number', + string: s => +s + } +} + diff --git a/number/add.mjs b/src/number/add.mjs similarity index 67% rename from number/add.mjs rename to src/number/add.mjs index 49c1f40..70be9bb 100644 --- a/number/add.mjs +++ b/src/number/add.mjs @@ -1,3 +1,5 @@ +export {Types} from './Types/number.mjs' + export const add = { '...number': [[], addends => addends.reduce((x,y) => x+y, 0)], } diff --git a/bigint/all.mjs b/src/number/all.mjs similarity index 73% rename from bigint/all.mjs rename to src/number/all.mjs index 395eb54..bf737f5 100644 --- a/bigint/all.mjs +++ b/src/number/all.mjs @@ -1,3 +1,4 @@ +export {Types} from './Types/number.mjs' export {add} from './add.mjs' export {negate} from './negate.mjs' export {subtract} from '../generic/subtract.mjs' diff --git a/src/number/negate.mjs b/src/number/negate.mjs new file mode 100644 index 0000000..aaf8bc0 --- /dev/null +++ b/src/number/negate.mjs @@ -0,0 +1,3 @@ +export { Types } from './Types/number.mjs' + +export const negate = {number: [[], n => -n]} diff --git a/pocomath.mjs b/src/pocomath.mjs similarity index 84% rename from pocomath.mjs rename to src/pocomath.mjs index 5a9bd4a..c5a81d4 100644 --- a/pocomath.mjs +++ b/src/pocomath.mjs @@ -1,5 +1,5 @@ /* Core of pocomath: generates the default instance */ -import PocomathInstance from './PocomathInstance.mjs' +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' diff --git a/test/_pocomath.mjs b/test/_pocomath.mjs index af24836..507d811 100644 --- a/test/_pocomath.mjs +++ b/test/_pocomath.mjs @@ -1,5 +1,5 @@ import assert from 'assert' -import math from '../pocomath.mjs' +import math from '../src/pocomath.mjs' describe('The default full pocomath instance "math"', () => { it('can subtract numbers', () => { diff --git a/test/_PocomathInstance.mjs b/test/core/_PocomathInstance.mjs similarity index 87% rename from test/_PocomathInstance.mjs rename to test/core/_PocomathInstance.mjs index d48cfd8..28176e2 100644 --- a/test/_PocomathInstance.mjs +++ b/test/core/_PocomathInstance.mjs @@ -1,5 +1,5 @@ import assert from 'assert' -import PocomathInstance from '../PocomathInstance.mjs' +import PocomathInstance from '../../src/core/PocomathInstance.mjs' describe('PocomathInstance', () => { it('creates an instance that can define typed-functions', () => { diff --git a/test/custom.mjs b/test/custom.mjs index a9d3df2..aabbb8c 100644 --- a/test/custom.mjs +++ b/test/custom.mjs @@ -1,26 +1,26 @@ 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 numberAdd from '../number/add.mjs' -import * as complex from '../complex/all.mjs' -import * as complexAdd from '../complex/add.mjs' -import * as complexNegate from '../complex/negate.mjs' -import extendToComplex from '../complex/extendToComplex.mjs' +import math from '../src/pocomath.mjs' +import PocomathInstance from '../src/core/PocomathInstance.mjs' +import * as numbers from '../src/number/all.mjs' +import * as numberAdd from '../src/number/add.mjs' +import * as complex from '../src/complex/all.mjs' +import * as complexAdd from '../src/complex/add.mjs' +import * as complexNegate from '../src/complex/negate.mjs' +import extendToComplex from '../src/complex/extendToComplex.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}) + // Not much we can call without any number types: + assert.deepStrictEqual(bw.complex(0, 3), {re: 0, im: 3}) + // Don't have a way to negate things, for example: + assert.throws(() => bw.negate(2), TypeError) }) it("can be assembled in any order", () => { bw.install(numbers) - typed.addConversion({from: 'string', to: 'number', convert: x => +x}) + bw.install({Types: {string: {test: s => typeof s === 'string'}}}) 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}) @@ -36,7 +36,7 @@ describe('A custom instance', () => { assert.strictEqual(pm.subtract(5, 10), -5) pm.install(complexAdd) pm.install(complexNegate) - // Should be enough to allow complex subtraction, as subtract is generic + // 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}) }) @@ -45,8 +45,9 @@ describe('A custom instance', () => { const cherry = new PocomathInstance('cherry') cherry.install(numberAdd) await extendToComplex(cherry) - // Now we have an instance that supports addition for number and complex - // and little else: + /* Now we have an instance that supports addition for number and complex + and little else: + */ assert.strictEqual(cherry.add(3, 4, 2), 9) assert.deepStrictEqual( cherry.add(cherry.complex(3, 3), 4, cherry.complex(2, 2)),