From c429c19dfeb1fbab747274c71dd9f3dfd8e49a4f Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sat, 30 Jul 2022 04:59:04 -0700 Subject: [PATCH] feat: Implement subtypes This should eventually be moved into typed-function itself, but for now it can be implemented on top of the existing typed-function. Uses subtypes to define (and error-check) gcd and lcm, which are only defined for integer arguments. Resolves #36. --- src/bigint/divide.mjs | 20 ++++------- src/bigint/isZero.mjs | 3 ++ src/bigint/multiply.mjs | 4 +-- src/bigint/native.mjs | 7 ++++ src/bigint/quotient.mjs | 13 +++++++ src/bigint/roundquotient.mjs | 15 ++++++++ src/complex/Types/Complex.mjs | 12 ++++++- src/complex/abs.mjs | 6 ++-- src/complex/absquare.mjs | 5 +++ src/complex/conjugate.mjs | 6 ++++ src/complex/gcd.mjs | 17 +++++++++ src/complex/isZero.mjs | 5 +++ src/complex/multiply.mjs | 14 ++++++++ src/complex/native.mjs | 10 ++++++ src/complex/negate.mjs | 8 ++--- src/complex/quotient.mjs | 5 +++ src/complex/roundquotient.mjs | 17 +++++++++ src/complex/zero.mjs | 5 +++ src/core/PocomathInstance.mjs | 65 +++++++++++++++++++++++++++++------ src/generic/arithmetic.mjs | 3 ++ src/generic/gcdType.mjs | 18 ++++++++++ src/generic/lcm.mjs | 6 ++++ src/generic/mod.mjs | 6 ++++ src/generic/multiply.mjs | 8 ++--- src/generic/square.mjs | 3 ++ src/number/Types/number.mjs | 5 +++ src/number/isZero.mjs | 3 ++ src/number/multiply.mjs | 4 +-- src/number/native.mjs | 6 ++++ src/number/quotient.mjs | 8 +++++ src/number/roundquotient.mjs | 8 +++++ test/bigint/_all.mjs | 12 +++++++ test/complex/_all.mjs | 6 ++++ test/custom.mjs | 1 + test/number/_all.mjs | 3 ++ 35 files changed, 294 insertions(+), 43 deletions(-) create mode 100644 src/bigint/isZero.mjs create mode 100644 src/bigint/quotient.mjs create mode 100644 src/bigint/roundquotient.mjs create mode 100644 src/complex/absquare.mjs create mode 100644 src/complex/conjugate.mjs create mode 100644 src/complex/gcd.mjs create mode 100644 src/complex/isZero.mjs create mode 100644 src/complex/multiply.mjs create mode 100644 src/complex/quotient.mjs create mode 100644 src/complex/roundquotient.mjs create mode 100644 src/complex/zero.mjs create mode 100644 src/generic/gcdType.mjs create mode 100644 src/generic/lcm.mjs create mode 100644 src/generic/mod.mjs create mode 100644 src/generic/square.mjs create mode 100644 src/number/isZero.mjs create mode 100644 src/number/quotient.mjs create mode 100644 src/number/roundquotient.mjs diff --git a/src/bigint/divide.mjs b/src/bigint/divide.mjs index 288230d..4554457 100644 --- a/src/bigint/divide.mjs +++ b/src/bigint/divide.mjs @@ -1,20 +1,12 @@ export * from './Types/bigint.mjs' export const divide = { - 'bigint,bigint': ({config, 'sign(bigint)': sgn}) => { - if (config.predictable) { - return (n, d) => { - if (sgn(n) === sgn(d)) return n/d - const quot = n/d - if (quot * d == n) return quot - return quot - 1n - } - } else { - return (n, d) => { - const quot = n/d - if (quot * d == n) return quot - return undefined - } + 'bigint,bigint': ({config, 'quotient(bigint,bigint)': quot}) => { + if (config.predictable) return quot + return (n, d) => { + const q = n/d + if (q * d == n) return q + return undefined } } } diff --git a/src/bigint/isZero.mjs b/src/bigint/isZero.mjs new file mode 100644 index 0000000..0efa71c --- /dev/null +++ b/src/bigint/isZero.mjs @@ -0,0 +1,3 @@ +export * from './Types/bigint.mjs' + +export const isZero = {bigint: () => b => b === 0n} diff --git a/src/bigint/multiply.mjs b/src/bigint/multiply.mjs index 7994abc..e19959e 100644 --- a/src/bigint/multiply.mjs +++ b/src/bigint/multiply.mjs @@ -1,5 +1,3 @@ export * from './Types/bigint.mjs' -export const multiply = { - '...bigint': () => multiplicands => multiplicands.reduce((x,y) => x*y, 1n) -} +export const multiply = {'bigint,bigint': () => (a,b) => a*b} diff --git a/src/bigint/native.mjs b/src/bigint/native.mjs index b09076a..93dc26f 100644 --- a/src/bigint/native.mjs +++ b/src/bigint/native.mjs @@ -1,9 +1,16 @@ +import gcdType from '../generic/gcdType.mjs' + export * from './Types/bigint.mjs' + export {add} from './add.mjs' export {divide} from './divide.mjs' +export const gcd = gcdType('bigint') +export {isZero} from './isZero.mjs' export {multiply} from './multiply.mjs' export {negate} from './negate.mjs' export {one} from './one.mjs' export {sign} from './sign.mjs' +export {quotient} from './quotient.mjs' +export {roundquotient} from './roundquotient.mjs' export {sqrt} from './sqrt.mjs' export {zero} from './zero.mjs' diff --git a/src/bigint/quotient.mjs b/src/bigint/quotient.mjs new file mode 100644 index 0000000..589adc3 --- /dev/null +++ b/src/bigint/quotient.mjs @@ -0,0 +1,13 @@ +export * from './Types/bigint.mjs' + +/* Returns the best integer approximation to n/d */ +export const quotient = { + 'bigint,bigint': ({'sign(bigint)': sgn}) => (n, d) => { + const dSgn = sgn(d) + if (dSgn === 0n) return 0n + if (sgn(n) === dSgn) return n/d + const quot = n/d + if (quot * d == n) return quot + return quot - 1n + } +} diff --git a/src/bigint/roundquotient.mjs b/src/bigint/roundquotient.mjs new file mode 100644 index 0000000..c6763a2 --- /dev/null +++ b/src/bigint/roundquotient.mjs @@ -0,0 +1,15 @@ +export * from './Types/bigint.mjs' + +/* Returns the closest integer approximation to n/d */ +export const roundquotient = { + 'bigint,bigint': ({'sign(bigint)': sgn}) => (n, d) => { + const dSgn = sgn(d) + if (dSgn === 0n) return 0n + const candidate = n/d + const rem = n - d*candidate + const absd = d*dSgn + if (2n * rem > absd) return candidate + dSgn + if (-2n * rem >= absd) return candidate - dSgn + return candidate + } +} diff --git a/src/complex/Types/Complex.mjs b/src/complex/Types/Complex.mjs index 05fe622..e7644b9 100644 --- a/src/complex/Types/Complex.mjs +++ b/src/complex/Types/Complex.mjs @@ -12,9 +12,19 @@ const Complex = new PocomathInstance('Complex') Complex.installType('Complex', { test: isComplex, from: { - number: x => ({re: x, im: 0}), + number: x => ({re: x, im: 0}) + } +}) +Complex.installType('GaussianInteger', { + test: z => typeof z.re == 'bigint' && typeof z.im == 'bigint', + refines: 'Complex', + from: { bigint: x => ({re: x, im: 0n}) } }) +Complex.promoteUnary = { + Complex: ({self,complex}) => z => complex(self(z.re), self(z.im)) +} + export {Complex} diff --git a/src/complex/abs.mjs b/src/complex/abs.mjs index 63aacd6..48d5a7b 100644 --- a/src/complex/abs.mjs +++ b/src/complex/abs.mjs @@ -1,5 +1,5 @@ export * from './Types/Complex.mjs' -export const abs = {Complex: ({sqrt, add, multiply}) => z => { - return sqrt(add(multiply(z.re, z.re), multiply(z.im, z.im))) -}} +export const abs = { + Complex: ({sqrt, 'absquare(Complex)': absq}) => z => sqrt(absq(z)) +} diff --git a/src/complex/absquare.mjs b/src/complex/absquare.mjs new file mode 100644 index 0000000..cfa1f30 --- /dev/null +++ b/src/complex/absquare.mjs @@ -0,0 +1,5 @@ +export * from './Types/Complex.mjs' + +export const absquare = { + Complex: ({add, square}) => z => add(square(z.re), square(z.im)) +} diff --git a/src/complex/conjugate.mjs b/src/complex/conjugate.mjs new file mode 100644 index 0000000..3139506 --- /dev/null +++ b/src/complex/conjugate.mjs @@ -0,0 +1,6 @@ +export * from './Types/Complex.mjs' + +export const conjugate = { + Complex: ({negate, complex}) => z => complex(z.re, negate(z.im)) +} + diff --git a/src/complex/gcd.mjs b/src/complex/gcd.mjs new file mode 100644 index 0000000..84aa849 --- /dev/null +++ b/src/complex/gcd.mjs @@ -0,0 +1,17 @@ +import PocomathInstance from '../core/PocomathInstance.mjs' +import * as Complex from './Types/Complex.mjs' +import gcdType from '../generic/gcdType.mjs' + +const imps = { + gcdComplexRaw: gcdType('Complex'), + gcd: { // Only return gcds with positive real part + 'Complex, Complex': ({gcdComplexRaw, sign, one, negate}) => (z,m) => { + const raw = gcdComplexRaw(z, m) + if (sign(raw.re) === one(raw.re)) return raw + return negate(raw) + } + } +} + +export const gcd = PocomathInstance.merge(Complex, imps) + diff --git a/src/complex/isZero.mjs b/src/complex/isZero.mjs new file mode 100644 index 0000000..1eaad7f --- /dev/null +++ b/src/complex/isZero.mjs @@ -0,0 +1,5 @@ +export * from './Types/Complex.mjs' + +export const isZero = { + Complex: ({self}) => z => self(z.re) && self(z.im) +} diff --git a/src/complex/multiply.mjs b/src/complex/multiply.mjs new file mode 100644 index 0000000..e1b46fe --- /dev/null +++ b/src/complex/multiply.mjs @@ -0,0 +1,14 @@ +export * from './Types/Complex.mjs' + +export const multiply = { + 'Complex,Complex': ({ + 'complex(any,any)': cplx, + add, + subtract, + self + }) => (w,z) => { + return cplx( + subtract(self(w.re, z.re), self(w.im, z.im)), + add(self(w.re, z.im), self(w.im, z.re))) + } +} diff --git a/src/complex/native.mjs b/src/complex/native.mjs index 03652ed..7bac8c2 100644 --- a/src/complex/native.mjs +++ b/src/complex/native.mjs @@ -1,8 +1,18 @@ +import gcdType from '../generic/gcdType.mjs' + export * from './Types/Complex.mjs' export {abs} from './abs.mjs' +export {absquare} from './absquare.mjs' export {add} from './add.mjs' +export {conjugate} from './conjugate.mjs' export {complex} from './complex.mjs' +export {gcd} from './gcd.mjs' +export {isZero} from './isZero.mjs' +export {multiply} from './multiply.mjs' export {negate} from './negate.mjs' +export {quotient} from './quotient.mjs' +export {roundquotient} from './roundquotient.mjs' export {sqrt} from './sqrt.mjs' +export {zero} from './zero.mjs' diff --git a/src/complex/negate.mjs b/src/complex/negate.mjs index 7ddaecc..75ad5e0 100644 --- a/src/complex/negate.mjs +++ b/src/complex/negate.mjs @@ -1,5 +1,5 @@ -export * from './Types/Complex.mjs' +import {Complex} from './Types/Complex.mjs' -export const negate = { - Complex: ({self}) => z => ({re: self(z.re), im: self(z.im)}) -} +const negate = Complex.promoteUnary + +export {Complex, negate} diff --git a/src/complex/quotient.mjs b/src/complex/quotient.mjs new file mode 100644 index 0000000..20090e0 --- /dev/null +++ b/src/complex/quotient.mjs @@ -0,0 +1,5 @@ +export * from './roundquotient.mjs' + +export const quotient = { + 'Complex,Complex': ({roundquotient}) => (w,z) => roundquotient(w,z) +} diff --git a/src/complex/roundquotient.mjs b/src/complex/roundquotient.mjs new file mode 100644 index 0000000..c9b2cbc --- /dev/null +++ b/src/complex/roundquotient.mjs @@ -0,0 +1,17 @@ +export * from './Types/Complex.mjs' + +export const roundquotient = { + 'Complex,Complex': ({ + 'isZero(Complex)': isZ, + conjugate, + 'multiply(Complex,Complex)': mult, + absquare, + self, + complex + }) => (n,d) => { + if (isZ(d)) return d + const cnum = mult(n, conjugate(d)) + const dreal = absquare(d) + return complex(self(cnum.re, dreal), self(cnum.im, dreal)) + } +} diff --git a/src/complex/zero.mjs b/src/complex/zero.mjs new file mode 100644 index 0000000..36cf676 --- /dev/null +++ b/src/complex/zero.mjs @@ -0,0 +1,5 @@ +import {Complex} from './Types/Complex.mjs' + +const zero = Complex.promoteUnary + +export {Complex, zero} diff --git a/src/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index 3c7e429..a6a2000 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -27,6 +27,7 @@ export default class PocomathInstance { this._typed = typed.create() this._typed.clear() this.Types = {any: anySpec} // dummy entry to track the default 'any' type + this._subtypes = {} // For each type, gives all of its (in)direct subtypes this._usedTypes = new Set() // all types that have occurred in a signature this._doomed = new Set() // for detecting circular reference this._config = {predictable: false} @@ -190,6 +191,9 @@ export default class PocomathInstance { * **to** this type to the corresponding conversion functions * - before: [optional] a list of types this should be added * before, in priority order + * - refines: [optional] the name of a type that this is a subtype + * of. This means the test is the conjunction of the given test and + * the supertype test, and that it must come before the supertype. */ /* * Implementation note: unlike _installFunctions below, we can make @@ -202,29 +206,68 @@ export default class PocomathInstance { } return } - let beforeType = 'any' - for (const other of spec.before || []) { - if (other in this.Types) { - beforeType = other - break + if (spec.refines && !(spec.refines in this.Types)) { + throw new SyntaxError( + `Cannot install ${type} before its supertype ${spec.refines}`) + } + let beforeType = spec.refines + if (!beforeType) { + beforeType = 'any' + for (const other of spec.before || []) { + if (other in this.Types) { + beforeType = other + break + } } } - this._typed.addTypes([{name: type, test: spec.test}], beforeType) + let testFn = spec.test + if (spec.refines) { + const supertypeTest = this.Types[spec.refines].test + testFn = entity => supertypeTest(entity) && spec.test(entity) + } + this._typed.addTypes([{name: type, test: testFn}], beforeType) + this.Types[type] = spec /* Now add conversions to this type */ for (const from in (spec.from || {})) { if (from in this.Types) { - this._typed.addConversion( - {from, to: type, convert: spec.from[from]}) + // add conversions from "from" to this one and all its supertypes: + let nextSuper = type + while (nextSuper) { + this._typed.addConversion( + {from, to: nextSuper, convert: spec.from[from]}) + nextSuper = this.Types[nextSuper].refines + } } } /* And add conversions from this type */ for (const to in this.Types) { if (type in (this.Types[to].from || {})) { - this._typed.addConversion( - {from: type, to, convert: this.Types[to].from[type]}) + if (spec.refines == to || spec.refines in this._subtypes[to]) { + throw new SyntaxError( + `Conversion of ${type} to its supertype ${to} disallowed.`) + } + let nextSuper = to + while (nextSuper) { + this._typed.addConversion({ + from: type, + to: nextSuper, + convert: this.Types[to].from[type] + }) + nextSuper = this.Types[nextSuper].refines + } } } - this.Types[type] = spec + if (spec.refines) { + this._typed.addConversion( + {from: type, to: spec.refines, convert: x => x}) + } + this._subtypes[type] = new Set() + // Update all the subtype sets of supertypes up the chain: + let nextSuper = spec.refines + while (nextSuper) { + this._subtypes[nextSuper].add(type) + nextSuper = this.Types[nextSuper].refines + } // rebundle anything that uses the new type: this._invalidateDependents(':' + type) } diff --git a/src/generic/arithmetic.mjs b/src/generic/arithmetic.mjs index e466b1f..8bcc904 100644 --- a/src/generic/arithmetic.mjs +++ b/src/generic/arithmetic.mjs @@ -1,7 +1,10 @@ export * from './Types/generic.mjs' +export {lcm} from './lcm.mjs' +export {mod} from './mod.mjs' export {multiply} from './multiply.mjs' export {divide} from './divide.mjs' export {sign} from './sign.mjs' export {sqrt} from './sqrt.mjs' +export {square} from './square.mjs' export {subtract} from './subtract.mjs' diff --git a/src/generic/gcdType.mjs b/src/generic/gcdType.mjs new file mode 100644 index 0000000..7406b0e --- /dev/null +++ b/src/generic/gcdType.mjs @@ -0,0 +1,18 @@ +/* Returns a object that defines the gcd for the given type */ +export default function(type) { + const producer = refs => { + const modder = refs[`mod(${type},${type})`] + const zeroTester = refs[`isZero(${type})`] + return (a,b) => { + while (!zeroTester(b)) { + const r = modder(a,b) + a = b + b = r + } + return a + } + } + const retval = {} + retval[`${type},${type}`] = producer + return retval +} diff --git a/src/generic/lcm.mjs b/src/generic/lcm.mjs new file mode 100644 index 0000000..f621024 --- /dev/null +++ b/src/generic/lcm.mjs @@ -0,0 +1,6 @@ +export const lcm = { + 'any,any': ({ + multiply, + quotient, + gcd}) => (a,b) => multiply(quotient(a, gcd(a,b)), b) +} diff --git a/src/generic/mod.mjs b/src/generic/mod.mjs new file mode 100644 index 0000000..669c5cb --- /dev/null +++ b/src/generic/mod.mjs @@ -0,0 +1,6 @@ +export const mod = { + 'any,any': ({ + subtract, + multiply, + quotient}) => (a,m) => subtract(a, multiply(m, quotient(a,m))) +} diff --git a/src/generic/multiply.mjs b/src/generic/multiply.mjs index dc078d9..a1dce22 100644 --- a/src/generic/multiply.mjs +++ b/src/generic/multiply.mjs @@ -4,9 +4,9 @@ export const multiply = { 'undefined': () => u => u, 'undefined,...any': () => (u, rest) => u, 'any,undefined': () => (x, u) => u, - 'any,undefined,...any': () => (x, u, rest) => u, - 'any,any,undefined': () => (x, y, u) => u, - 'any,any,undefined,...any': () => (x, y, u, rest) => u - // Bit of a hack since this should go on indefinitely... + 'any,any,...any': ({self}) => (a,b,rest) => { + const later = [b, ...rest] + return later.reduce((x,y) => self(x,y), a) + } } diff --git a/src/generic/square.mjs b/src/generic/square.mjs new file mode 100644 index 0000000..311234a --- /dev/null +++ b/src/generic/square.mjs @@ -0,0 +1,3 @@ +export const square = { + any: ({multiply}) => x => multiply(x,x) +} diff --git a/src/number/Types/number.mjs b/src/number/Types/number.mjs index e068f67..5b0d694 100644 --- a/src/number/Types/number.mjs +++ b/src/number/Types/number.mjs @@ -5,4 +5,9 @@ Number.installType('number', { test: n => typeof n === 'number', from: {string: s => +s} }) +Number.installType('NumInt', { + refines: 'number', + test: i => isFinite(i) && i === Math.round(i) +}) + export {Number} diff --git a/src/number/isZero.mjs b/src/number/isZero.mjs new file mode 100644 index 0000000..ac98ce2 --- /dev/null +++ b/src/number/isZero.mjs @@ -0,0 +1,3 @@ +export * from './Types/number.mjs' + +export const isZero = {number: () => n => n === 0} diff --git a/src/number/multiply.mjs b/src/number/multiply.mjs index 8d7f797..80573d1 100644 --- a/src/number/multiply.mjs +++ b/src/number/multiply.mjs @@ -1,5 +1,3 @@ export * from './Types/number.mjs' -export const multiply = { - '...number': () => multiplicands => multiplicands.reduce((x,y) => x*y, 1), -} +export const multiply = {'number,number': () => (m,n) => m*n} diff --git a/src/number/native.mjs b/src/number/native.mjs index 1053579..484b2c6 100644 --- a/src/number/native.mjs +++ b/src/number/native.mjs @@ -1,10 +1,16 @@ +import gcdType from '../generic/gcdType.mjs' + export * from './Types/number.mjs' export {abs} from './abs.mjs' export {add} from './add.mjs' +export const gcd = gcdType('NumInt') export {invert} from './invert.mjs' +export {isZero} from './isZero.mjs' export {multiply} from './multiply.mjs' export {negate} from './negate.mjs' export {one} from './one.mjs' +export {quotient} from './quotient.mjs' +export {roundquotient} from './roundquotient.mjs' export {sqrt} from './sqrt.mjs' export {zero} from './zero.mjs' diff --git a/src/number/quotient.mjs b/src/number/quotient.mjs new file mode 100644 index 0000000..d86b832 --- /dev/null +++ b/src/number/quotient.mjs @@ -0,0 +1,8 @@ +export * from './Types/number.mjs' + +export const quotient = { + 'number,number': () => (n,d) => { + if (d === 0) return d + return Math.floor(n/d) + } +} diff --git a/src/number/roundquotient.mjs b/src/number/roundquotient.mjs new file mode 100644 index 0000000..401d499 --- /dev/null +++ b/src/number/roundquotient.mjs @@ -0,0 +1,8 @@ +export * from './Types/number.mjs' + +export const roundquotient = { + 'number,number': () => (n,d) => { + if (d === 0) return d + return Math.round(n/d) + } +} diff --git a/test/bigint/_all.mjs b/test/bigint/_all.mjs index 9c80df3..29b3d27 100644 --- a/test/bigint/_all.mjs +++ b/test/bigint/_all.mjs @@ -52,4 +52,16 @@ describe('bigint', () => { assert.deepStrictEqual(bo.sqrt(-3249n), bo.complex(0n, 57n)) }) + it('computes gcd', () => { + assert.strictEqual(math.gcd(105n, 70n), 35n) + }) + + it('computes lcm', () => { + assert.strictEqual(math.lcm(105n, 70n), 210n) + assert.strictEqual(math.lcm(15n, 60n), 60n) + assert.strictEqual(math.lcm(0n, 17n), 0n) + assert.strictEqual(math.lcm(20n, 0n), 0n) + assert.strictEqual(math.lcm(0n, 0n), 0n) + }) + }) diff --git a/test/complex/_all.mjs b/test/complex/_all.mjs index 1d49855..a5e8833 100644 --- a/test/complex/_all.mjs +++ b/test/complex/_all.mjs @@ -29,4 +29,10 @@ describe('complex', () => { math.complex(ms.negate(ms.sqrt(0.5)), ms.sqrt(0.5))) }) + it('computes gcd', () => { + assert.deepStrictEqual( + math.gcd(math.complex(53n, 56n), math.complex(47n, -13n)), + math.complex(4n, 5n)) + }) + }) diff --git a/test/custom.mjs b/test/custom.mjs index 9292437..ab26563 100644 --- a/test/custom.mjs +++ b/test/custom.mjs @@ -39,6 +39,7 @@ describe('A custom instance', () => { assert.strictEqual(pm.subtract(5, 10), -5) pm.install(complexAdd) pm.install(complexNegate) + pm.install(complexComplex) // 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}) diff --git a/test/number/_all.mjs b/test/number/_all.mjs index 1af4ed4..20fab68 100644 --- a/test/number/_all.mjs +++ b/test/number/_all.mjs @@ -27,4 +27,7 @@ describe('number', () => { assert.deepStrictEqual(no.sqrt(-16), no.complex(0,4)) }) + it('computes gcd', () => { + assert.strictEqual(math.gcd(15, 35), 5) + }) }) -- 2.34.1