From 36e7b750ce9757397c8ad50fc65af6ebc8907e3f Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Mon, 1 Aug 2022 02:57:38 -0700 Subject: [PATCH] refactor: Make generics more strict via templates This commit also adds a built-in `typeOf` function to every PocomathInstance. It also adds a notation (prefix '!') for "eager" templeates that instantiate themselves for all types immediately upon definition. --- src/bigint/all.mjs | 2 +- src/bigint/compare.mjs | 5 +++ src/bigint/native.mjs | 1 + src/complex/add.mjs | 10 ++++++ src/complex/equal.mjs | 19 +++++++++++ src/complex/native.mjs | 1 + src/complex/sqrt.mjs | 12 ++----- src/core/PocomathInstance.mjs | 60 +++++++++++++++++++++++++---------- src/generic/all.mjs | 1 + src/generic/divide.mjs | 5 ++- src/generic/gcdType.mjs | 4 +++ src/generic/lcm.mjs | 9 +++--- src/generic/mod.mjs | 9 +++--- src/generic/multiply.mjs | 1 + src/generic/relational.mjs | 54 +++++++++++++++++++++++++++++++ src/generic/sign.mjs | 5 +-- src/generic/square.mjs | 2 +- src/number/all.mjs | 2 +- src/number/compare.mjs | 52 ++++++++++++++++++++++++++++++ src/number/native.mjs | 1 + test/_pocomath.mjs | 8 +++-- test/complex/_all.mjs | 7 ++++ test/number/_all.mjs | 7 ++++ 23 files changed, 233 insertions(+), 44 deletions(-) create mode 100644 src/bigint/compare.mjs create mode 100644 src/complex/equal.mjs create mode 100644 src/generic/relational.mjs create mode 100644 src/number/compare.mjs diff --git a/src/bigint/all.mjs b/src/bigint/all.mjs index 0d6e517..74dce18 100644 --- a/src/bigint/all.mjs +++ b/src/bigint/all.mjs @@ -1,5 +1,5 @@ import PocomathInstance from '../core/PocomathInstance.mjs' import * as bigints from './native.mjs' -import * as generic from '../generic/arithmetic.mjs' +import * as generic from '../generic/all.mjs' export default PocomathInstance.merge('bigint', bigints, generic) diff --git a/src/bigint/compare.mjs b/src/bigint/compare.mjs new file mode 100644 index 0000000..ab830ab --- /dev/null +++ b/src/bigint/compare.mjs @@ -0,0 +1,5 @@ +export * from './Types/bigint.mjs' + +export const compare = { + 'bigint,bigint': () => (a,b) => a === b ? 0n : (a > b ? 1n : -1n) +} diff --git a/src/bigint/native.mjs b/src/bigint/native.mjs index 93dc26f..dc12d9e 100644 --- a/src/bigint/native.mjs +++ b/src/bigint/native.mjs @@ -3,6 +3,7 @@ import gcdType from '../generic/gcdType.mjs' export * from './Types/bigint.mjs' export {add} from './add.mjs' +export {compare} from './compare.mjs' export {divide} from './divide.mjs' export const gcd = gcdType('bigint') export {isZero} from './isZero.mjs' diff --git a/src/complex/add.mjs b/src/complex/add.mjs index 8f26167..1afd22f 100644 --- a/src/complex/add.mjs +++ b/src/complex/add.mjs @@ -1,10 +1,20 @@ export * from './Types/Complex.mjs' export const add = { + /* Relying on conversions for both complex + number and complex + bigint + * leads to an infinite loop when adding a number and a bigint, since they + * both convert to Complex. + */ 'Complex,number': ({ 'self(number,number)': addNum, 'complex(any,any)': cplx }) => (z,x) => cplx(addNum(z.re, x), z.im), + + 'Complex,bigint': ({ + 'self(bigint,bigint)': addBigInt, + 'complex(any,any)': cplx + }) => (z,x) => cplx(addBigInt(z.re, x), z.im), + 'Complex,Complex': ({ self, 'complex(any,any)': cplx diff --git a/src/complex/equal.mjs b/src/complex/equal.mjs new file mode 100644 index 0000000..4eca63f --- /dev/null +++ b/src/complex/equal.mjs @@ -0,0 +1,19 @@ +export * from './Types/Complex.mjs' + +export const equal = { + 'Complex,number': ({ + 'isZero(number)': isZ, + 'self(number,number)': eqNum + }) => (z, x) => eqNum(z.re, x) && isZ(z.im), + + 'Complex,bigint': ({ + 'isZero(bigint)': isZ, + 'self(bigint,bigint)': eqBigInt + }) => (z, b) => eqBigInt(z.re, b) && isZ(z.im), + + 'Complex,Complex': ({self}) => (w,z) => self(w.re, z.re) && self(w.im, z.im), + + 'GaussianInteger,GaussianInteger': ({ + 'self(bigint,bigint)': eq + }) => (a,b) => eq(a.re, b.re) && eq(a.im, b.im) +} diff --git a/src/complex/native.mjs b/src/complex/native.mjs index 8938136..70d6b14 100644 --- a/src/complex/native.mjs +++ b/src/complex/native.mjs @@ -7,6 +7,7 @@ export {absquare} from './absquare.mjs' export {add} from './add.mjs' export {conjugate} from './conjugate.mjs' export {complex} from './complex.mjs' +export {equal} from './equal.mjs' export {gcd} from './gcd.mjs' export {invert} from './invert.mjs' export {isZero} from './isZero.mjs' diff --git a/src/complex/sqrt.mjs b/src/complex/sqrt.mjs index c3c3ab5..3557c6d 100644 --- a/src/complex/sqrt.mjs +++ b/src/complex/sqrt.mjs @@ -3,7 +3,7 @@ export * from './Types/Complex.mjs' export const sqrt = { Complex: ({ config, - zero, + isZero, sign, one, add, @@ -16,11 +16,8 @@ export const sqrt = { }) => { if (config.predictable) { return z => { - const imZero = zero(z.im) - const imSign = sign(z.im) const reOne = one(z.re) - const reSign = sign(z.re) - if (imSign === imZero && reSign === reOne) return complex(self(z.re)) + if (isZero(z.im) && sign(z.re) === reOne) return complex(self(z.re)) const reTwo = add(reOne, reOne) return complex( multiply(sign(z.im), self(divide(add(abs(z),z.re), reTwo))), @@ -29,11 +26,8 @@ export const sqrt = { } } return z => { - const imZero = zero(z.im) - const imSign = sign(z.im) const reOne = one(z.re) - const reSign = sign(z.re) - if (imSign === imZero && reSign === reOne) return self(z.re) + if (isZero(z.im) && sign(z.re) === reOne) return self(z.re) const reTwo = add(reOne, reOne) return complex( multiply(sign(z.im), self(divide(add(abs(z),z.re), reTwo))), diff --git a/src/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index 6ef3288..180ca17 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -40,7 +40,8 @@ export default class PocomathInstance { /* List of types installed in the instance. We start with just dummies * for the 'any' type and for type parameters: */ - this.Types = {any: anySpec, T: anySpec} + this.Types = {any: anySpec} + this.Types[theTemplateParam] = anySpec this._subtypes = {} // For each type, gives all of its (in)direct subtypes /* The following gives for each type, a set of all types that could * match in typed-function's dispatch algorithm before the given type. @@ -51,7 +52,7 @@ export default class PocomathInstance { this._priorTypes = {} this._usedTypes = new Set() // all types that have occurred in a signature this._doomed = new Set() // for detecting circular reference - this._config = {predictable: false} + this._config = {predictable: false, epsilon: 1e-12} const self = this this.config = new Proxy(this._config, { get: (target, property) => target[property], @@ -106,6 +107,20 @@ export default class PocomathInstance { * refer to just adding two numbers. In this case, it is of course * necessary to specify an alias to be able to refer to the supplied * operation in the body of the implementation. + * + * You can specify template implementations. If any item in the signature + * contains the word 'T' (currently the only allowed type parameter) then + * the signature/implementation is a template. The T can match any type + * of argument, and it may appear in the dependencies, where it is + * replaced by the matching type. A bare 'T' in the dependencies will be + * supplied with the name of the type as its value. See the implementation + * of `subtract` for an example. + * Usually templates are instantiated as needed, but for some heavily + * used functions, or functions with non-template signatures that refer + * to signatures generated from a template, it makes more sense to just + * instantiate the template immediately for all known types. This eager + * instantiation can be accomplished by prefixin the signature with an + * exclamation point. */ install(ops) { if (ops instanceof PocomathInstance) { @@ -273,6 +288,7 @@ export default class PocomathInstance { while (nextSuper) { this._typed.addConversion( {from, to: nextSuper, convert: spec.from[from]}) + this._invalidateDependents(':' + nextSuper) this._priorTypes[nextSuper].add(from) nextSuper = this.Types[nextSuper].refines } @@ -292,6 +308,7 @@ export default class PocomathInstance { to: nextSuper, convert: this.Types[to].from[type] }) + this._invalidateDependents(':' + nextSuper) this._priorTypes[nextSuper].add(type) nextSuper = this.Types[nextSuper].refines } @@ -305,13 +322,12 @@ export default class PocomathInstance { while (nextSuper) { this._typed.addConversion( {from: type, to: nextSuper, convert: x => x}) + this._invalidateDependents(':' + nextSuper) this._priorTypes[nextSuper].add(type) this._subtypes[nextSuper].add(type) nextSuper = this.Types[nextSuper].refines } - // rebundle anything that uses the new type: - this._invalidateDependents(':' + type) // update the typeOf function const imp = {} imp[type] = {uses: new Set(), does: () => () => type} @@ -436,13 +452,30 @@ export default class PocomathInstance { continue } /* It's a template, have to instantiate */ - /* First, add the known instantiations */ + /* First, add the known instantiations, gathering all types needed */ if (!('instantiations' in behavior)) { behavior.instantiations = new Set() } - for (const instType of behavior.instantiations) { + let instantiationSet = new Set() + let trimSignature = rawSignature + if (rawSignature.charAt(0) === '!') { + trimSignature = trimSignature.slice(1) + instantiationSet = this._usedTypes + } else { + for (const instType of behavior.instantiations) { + instantiationSet.add(instType) + for (const other of this._priorTypes[instType]) { + instantiationSet.add(other) + } + } + } + + for (const instType of instantiationSet) { + if (this.Types[instType] === anySpec) continue const signature = - substituteInSig(rawSignature, theTemplateParam, instType) + substituteInSig(trimSignature, theTemplateParam, instType) + /* Don't override an explicit implementation: */ + if (signature in imps) continue const uses = new Set() for (const dep of behavior.uses) { if (this._templateParam(dep)) continue @@ -465,11 +498,12 @@ export default class PocomathInstance { this._addTFimplementation(tf_imps, signature, {uses, does: patch}) } /* Now add the catchall signature */ - const signature = substituteInSig(rawSignature, theTemplateParam, 'any') + const signature = substituteInSig( + trimSignature, theTemplateParam, 'any') /* The catchall signature has to detect the actual type of the call * and add the new instantiations */ - const argTypes = rawSignature.split(',') + const argTypes = trimSignature.split(',') let exemplar = -1 for (let i = 0; i < argTypes.length; ++i) { const argType = argTypes[i].trim() @@ -487,14 +521,8 @@ export default class PocomathInstance { const example = args[exemplar] const instantiateFor = self.typeOf(example) refs[theTemplateParam] = instantiateFor - const instCount = behavior.instantiations.size - for (const earlier of self._priorTypes[instantiateFor]) { - behavior.instantiations.add(earlier) - } behavior.instantiations.add(instantiateFor) - if (behavior.instantiations.size > instCount) { - self._invalidate(name) - } + self._invalidate(name) // And for now, we have to rely on the "any" implementation. Hope // it matches the instantiated one! return behavior.does(refs)(...args) diff --git a/src/generic/all.mjs b/src/generic/all.mjs index db678dd..19ba165 100644 --- a/src/generic/all.mjs +++ b/src/generic/all.mjs @@ -1 +1,2 @@ export * from './arithmetic.mjs' +export * from './relational.mjs' diff --git a/src/generic/divide.mjs b/src/generic/divide.mjs index 4dcfc89..1aee89b 100644 --- a/src/generic/divide.mjs +++ b/src/generic/divide.mjs @@ -1,4 +1,7 @@ export const divide = { - 'any,any': ({multiply, invert}) => (x, y) => multiply(x, invert(y)) + 'T,T': ({ + 'multiply(T,T)': multT, + 'invert(T)': invT + }) => (x, y) => multT(x, invT(y)) } diff --git a/src/generic/gcdType.mjs b/src/generic/gcdType.mjs index 7406b0e..1ca16ab 100644 --- a/src/generic/gcdType.mjs +++ b/src/generic/gcdType.mjs @@ -1,3 +1,7 @@ +/* Note we do not use a template here so that we can explicitly control + * which types this is instantiated for, namely the "integer" types, and + * not simply allow Pocomath to generate instances for any type it encounters. + */ /* Returns a object that defines the gcd for the given type */ export default function(type) { const producer = refs => { diff --git a/src/generic/lcm.mjs b/src/generic/lcm.mjs index f621024..adc3dfb 100644 --- a/src/generic/lcm.mjs +++ b/src/generic/lcm.mjs @@ -1,6 +1,7 @@ export const lcm = { - 'any,any': ({ - multiply, - quotient, - gcd}) => (a,b) => multiply(quotient(a, gcd(a,b)), b) + 'T,T': ({ + 'multiply(T,T)': multT, + 'quotient(T,T)': quotT, + 'gcd(T,T)': gcdT + }) => (a,b) => multT(quotT(a, gcdT(a,b)), b) } diff --git a/src/generic/mod.mjs b/src/generic/mod.mjs index 669c5cb..84af4e6 100644 --- a/src/generic/mod.mjs +++ b/src/generic/mod.mjs @@ -1,6 +1,7 @@ export const mod = { - 'any,any': ({ - subtract, - multiply, - quotient}) => (a,m) => subtract(a, multiply(m, quotient(a,m))) + 'T,T': ({ + 'subtract(T,T)': subT, + 'multiply(T,T)': multT, + 'quotient(T,T)': quotT + }) => (a,m) => subT(a, multT(m, quotT(a,m))) } diff --git a/src/generic/multiply.mjs b/src/generic/multiply.mjs index a1dce22..63d196a 100644 --- a/src/generic/multiply.mjs +++ b/src/generic/multiply.mjs @@ -3,6 +3,7 @@ export * from './Types/generic.mjs' export const multiply = { 'undefined': () => u => u, 'undefined,...any': () => (u, rest) => u, + any: () => x => x, 'any,undefined': () => (x, u) => u, 'any,any,...any': ({self}) => (a,b,rest) => { const later = [b, ...rest] diff --git a/src/generic/relational.mjs b/src/generic/relational.mjs new file mode 100644 index 0000000..368f394 --- /dev/null +++ b/src/generic/relational.mjs @@ -0,0 +1,54 @@ +export const compare = { + 'undefined,undefined': () => () => 0 +} + +export const isZero = { + 'undefined': () => u => u === 0 +} + +export const equal = { + '!T,T': ({ + 'compare(T,T)': cmp, + 'isZero(T)': isZ + }) => (x,y) => isZ(cmp(x,y)) +} + +export const unequal = { + 'T,T': ({'equal(T.T)': eq}) => (x,y) => !(eq(x,y)) +} + +export const larger = { + 'T,T': ({ + 'compare(T,T)': cmp, + 'one(T)' : uno + }) => (x,y) => cmp(x,y) === uno(y) +} + +export const largerEq = { + 'T,T': ({ + 'compare(T,T)': cmp, + 'one(T)' : uno, + 'isZero(T)' : isZ + }) => (x,y) => { + const c = cmp(x,y) + return isZ(c) || c === uno(y) + } +} + +export const smaller = { + 'T,T': ({ + 'compare(T,T)': cmp, + 'one(T)' : uno, + 'isZero(T)' : isZ + }) => (x,y) => { + const c = cmp(x,y) + return !isZ(c) && c !== uno(y) + } +} + +export const smallerEq = { + 'T,T': ({ + 'compare(T,T)': cmp, + 'one(T)' : uno + }) => (x,y) => cmp(x,y) !== uno(y) +} diff --git a/src/generic/sign.mjs b/src/generic/sign.mjs index c8133fd..769e2c9 100644 --- a/src/generic/sign.mjs +++ b/src/generic/sign.mjs @@ -1,6 +1,3 @@ export const sign = { - any: ({negate, divide, abs}) => x => { - if (x === negate(x)) return x // zero - return divide(x, abs(x)) - } + T: ({'compare(T,T)': cmp, 'zero(T)': Z}) => x => cmp(x, Z(x)) } diff --git a/src/generic/square.mjs b/src/generic/square.mjs index 311234a..53fd6c2 100644 --- a/src/generic/square.mjs +++ b/src/generic/square.mjs @@ -1,3 +1,3 @@ export const square = { - any: ({multiply}) => x => multiply(x,x) + T: ({'multiply(T,T)': multT}) => x => multT(x,x) } diff --git a/src/number/all.mjs b/src/number/all.mjs index 54db15a..4a1228e 100644 --- a/src/number/all.mjs +++ b/src/number/all.mjs @@ -1,6 +1,6 @@ import PocomathInstance from '../core/PocomathInstance.mjs' import * as numbers from './native.mjs' -import * as generic from '../generic/arithmetic.mjs' +import * as generic from '../generic/all.mjs' export default PocomathInstance.merge('number', numbers, generic) diff --git a/src/number/compare.mjs b/src/number/compare.mjs new file mode 100644 index 0000000..4dc865b --- /dev/null +++ b/src/number/compare.mjs @@ -0,0 +1,52 @@ +/* Lifted from mathjs/src/utils/number.js */ +/** + * Minimum number added to one that makes the result different than one + */ +export const DBL_EPSILON = Number.EPSILON || 2.2204460492503130808472633361816E-16 + +/** + * Compares two floating point numbers. + * @param {number} x First value to compare + * @param {number} y Second value to compare + * @param {number} [epsilon] The maximum relative difference between x and y + * If epsilon is undefined or null, the function will + * test whether x and y are exactly equal. + * @return {boolean} whether the two numbers are nearly equal +*/ +function nearlyEqual (x, y, epsilon) { + // if epsilon is null or undefined, test whether x and y are exactly equal + if (epsilon === null || epsilon === undefined) { + return x === y + } + + if (x === y) { + return true + } + + // NaN + if (isNaN(x) || isNaN(y)) { + return false + } + + // at this point x and y should be finite + if (isFinite(x) && isFinite(y)) { + // check numbers are very close, needed when comparing numbers near zero + const diff = Math.abs(x - y) + if (diff < DBL_EPSILON) { + return true + } else { + // use relative error + return diff <= Math.max(Math.abs(x), Math.abs(y)) * epsilon + } + } + + // Infinite and Number or negative Infinite and positive Infinite cases + return false +} +/* End of copied section */ + +export const compare = { + 'number,number': ({ + config + }) => (x,y) => nearlyEqual(x, y, config.epsilon) ? 0 : (x > y ? 1 : -1) +} diff --git a/src/number/native.mjs b/src/number/native.mjs index 484b2c6..1404ab4 100644 --- a/src/number/native.mjs +++ b/src/number/native.mjs @@ -4,6 +4,7 @@ export * from './Types/number.mjs' export {abs} from './abs.mjs' export {add} from './add.mjs' +export {compare} from './compare.mjs' export const gcd = gcdType('NumInt') export {invert} from './invert.mjs' export {isZero} from './isZero.mjs' diff --git a/test/_pocomath.mjs b/test/_pocomath.mjs index b17ec5c..1a6fabe 100644 --- a/test/_pocomath.mjs +++ b/test/_pocomath.mjs @@ -1,4 +1,5 @@ import assert from 'assert' +import PocomathInstance from '../src/core/PocomathInstance.mjs' import math from '../src/pocomath.mjs' describe('The default full pocomath instance "math"', () => { @@ -37,16 +38,17 @@ describe('The default full pocomath instance "math"', () => { }) it('can be extended', () => { - math.installType('stringK', { + const stretch = PocomathInstance.merge(math) // clone to not pollute math + stretch.installType('stringK', { test: s => typeof s === 'string' && s.charAt(0) === 'K', before: ['string'] }) - math.install({ + stretch.install({ add: { '...stringK': () => addends => addends.reduce((x,y) => x+y, '') }, }) - assert.strictEqual(math.add('Kilroy','K is here'), 'KilroyK is here') + assert.strictEqual(stretch.add('Kilroy','K is here'), 'KilroyK is here') }) it('handles complex numbers', () => { diff --git a/test/complex/_all.mjs b/test/complex/_all.mjs index 023ff1c..ae146b6 100644 --- a/test/complex/_all.mjs +++ b/test/complex/_all.mjs @@ -38,6 +38,13 @@ describe('complex', () => { math.complex(ms.negate(ms.sqrt(0.5)), ms.sqrt(0.5))) }) + it('checks for equality', () => { + assert.ok(math.equal(math.complex(3,0), 3)) + assert.ok(math.equal(math.complex(3,2), math.complex(3, 2))) + assert.ok(!(math.equal(math.complex(45n, 3n), math.complex(45n, -3n)))) + assert.ok(!(math.equal(math.complex(45n, 3n), 45n))) + }) + it('computes gcd', () => { assert.deepStrictEqual( math.gcd(math.complex(53n, 56n), math.complex(47n, -13n)), diff --git a/test/number/_all.mjs b/test/number/_all.mjs index 20fab68..f8e7312 100644 --- a/test/number/_all.mjs +++ b/test/number/_all.mjs @@ -30,4 +30,11 @@ describe('number', () => { it('computes gcd', () => { assert.strictEqual(math.gcd(15, 35), 5) }) + + it('compares numbers', () => { + assert.ok(math.smaller(12,13.5)) + assert.ok(math.equal(Infinity, Infinity)) + assert.ok(math.largerEq(12.5, math.divide(25,2))) + }) + })