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.
This commit is contained in:
Glen Whitney 2022-07-30 04:59:04 -07:00
parent 4d38f4161c
commit c429c19dfe
35 changed files with 294 additions and 43 deletions

View File

@ -1,20 +1,12 @@
export * from './Types/bigint.mjs' export * from './Types/bigint.mjs'
export const divide = { export const divide = {
'bigint,bigint': ({config, 'sign(bigint)': sgn}) => { 'bigint,bigint': ({config, 'quotient(bigint,bigint)': quot}) => {
if (config.predictable) { if (config.predictable) return quot
return (n, d) => { return (n, d) => {
if (sgn(n) === sgn(d)) return n/d const q = n/d
const quot = n/d if (q * d == n) return q
if (quot * d == n) return quot return undefined
return quot - 1n
}
} else {
return (n, d) => {
const quot = n/d
if (quot * d == n) return quot
return undefined
}
} }
} }
} }

3
src/bigint/isZero.mjs Normal file
View File

@ -0,0 +1,3 @@
export * from './Types/bigint.mjs'
export const isZero = {bigint: () => b => b === 0n}

View File

@ -1,5 +1,3 @@
export * from './Types/bigint.mjs' export * from './Types/bigint.mjs'
export const multiply = { export const multiply = {'bigint,bigint': () => (a,b) => a*b}
'...bigint': () => multiplicands => multiplicands.reduce((x,y) => x*y, 1n)
}

View File

@ -1,9 +1,16 @@
import gcdType from '../generic/gcdType.mjs'
export * from './Types/bigint.mjs' export * from './Types/bigint.mjs'
export {add} from './add.mjs' export {add} from './add.mjs'
export {divide} from './divide.mjs' export {divide} from './divide.mjs'
export const gcd = gcdType('bigint')
export {isZero} from './isZero.mjs'
export {multiply} from './multiply.mjs' export {multiply} from './multiply.mjs'
export {negate} from './negate.mjs' export {negate} from './negate.mjs'
export {one} from './one.mjs' export {one} from './one.mjs'
export {sign} from './sign.mjs' export {sign} from './sign.mjs'
export {quotient} from './quotient.mjs'
export {roundquotient} from './roundquotient.mjs'
export {sqrt} from './sqrt.mjs' export {sqrt} from './sqrt.mjs'
export {zero} from './zero.mjs' export {zero} from './zero.mjs'

13
src/bigint/quotient.mjs Normal file
View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -12,9 +12,19 @@ const Complex = new PocomathInstance('Complex')
Complex.installType('Complex', { Complex.installType('Complex', {
test: isComplex, test: isComplex,
from: { 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}) bigint: x => ({re: x, im: 0n})
} }
}) })
Complex.promoteUnary = {
Complex: ({self,complex}) => z => complex(self(z.re), self(z.im))
}
export {Complex} export {Complex}

View File

@ -1,5 +1,5 @@
export * from './Types/Complex.mjs' export * from './Types/Complex.mjs'
export const abs = {Complex: ({sqrt, add, multiply}) => z => { export const abs = {
return sqrt(add(multiply(z.re, z.re), multiply(z.im, z.im))) Complex: ({sqrt, 'absquare(Complex)': absq}) => z => sqrt(absq(z))
}} }

5
src/complex/absquare.mjs Normal file
View File

@ -0,0 +1,5 @@
export * from './Types/Complex.mjs'
export const absquare = {
Complex: ({add, square}) => z => add(square(z.re), square(z.im))
}

View File

@ -0,0 +1,6 @@
export * from './Types/Complex.mjs'
export const conjugate = {
Complex: ({negate, complex}) => z => complex(z.re, negate(z.im))
}

17
src/complex/gcd.mjs Normal file
View File

@ -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)

5
src/complex/isZero.mjs Normal file
View File

@ -0,0 +1,5 @@
export * from './Types/Complex.mjs'
export const isZero = {
Complex: ({self}) => z => self(z.re) && self(z.im)
}

14
src/complex/multiply.mjs Normal file
View File

@ -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)))
}
}

View File

@ -1,8 +1,18 @@
import gcdType from '../generic/gcdType.mjs'
export * from './Types/Complex.mjs' export * from './Types/Complex.mjs'
export {abs} from './abs.mjs' export {abs} from './abs.mjs'
export {absquare} from './absquare.mjs'
export {add} from './add.mjs' export {add} from './add.mjs'
export {conjugate} from './conjugate.mjs'
export {complex} from './complex.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 {negate} from './negate.mjs'
export {quotient} from './quotient.mjs'
export {roundquotient} from './roundquotient.mjs'
export {sqrt} from './sqrt.mjs' export {sqrt} from './sqrt.mjs'
export {zero} from './zero.mjs'

View File

@ -1,5 +1,5 @@
export * from './Types/Complex.mjs' import {Complex} from './Types/Complex.mjs'
export const negate = { const negate = Complex.promoteUnary
Complex: ({self}) => z => ({re: self(z.re), im: self(z.im)})
} export {Complex, negate}

5
src/complex/quotient.mjs Normal file
View File

@ -0,0 +1,5 @@
export * from './roundquotient.mjs'
export const quotient = {
'Complex,Complex': ({roundquotient}) => (w,z) => roundquotient(w,z)
}

View File

@ -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))
}
}

5
src/complex/zero.mjs Normal file
View File

@ -0,0 +1,5 @@
import {Complex} from './Types/Complex.mjs'
const zero = Complex.promoteUnary
export {Complex, zero}

View File

@ -27,6 +27,7 @@ export default class PocomathInstance {
this._typed = typed.create() this._typed = typed.create()
this._typed.clear() this._typed.clear()
this.Types = {any: anySpec} // dummy entry to track the default 'any' type 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._usedTypes = new Set() // all types that have occurred in a signature
this._doomed = new Set() // for detecting circular reference this._doomed = new Set() // for detecting circular reference
this._config = {predictable: false} this._config = {predictable: false}
@ -190,6 +191,9 @@ export default class PocomathInstance {
* **to** this type to the corresponding conversion functions * **to** this type to the corresponding conversion functions
* - before: [optional] a list of types this should be added * - before: [optional] a list of types this should be added
* before, in priority order * 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 * Implementation note: unlike _installFunctions below, we can make
@ -202,29 +206,68 @@ export default class PocomathInstance {
} }
return return
} }
let beforeType = 'any' if (spec.refines && !(spec.refines in this.Types)) {
for (const other of spec.before || []) { throw new SyntaxError(
if (other in this.Types) { `Cannot install ${type} before its supertype ${spec.refines}`)
beforeType = other }
break 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 */ /* Now add conversions to this type */
for (const from in (spec.from || {})) { for (const from in (spec.from || {})) {
if (from in this.Types) { if (from in this.Types) {
this._typed.addConversion( // add conversions from "from" to this one and all its supertypes:
{from, to: type, convert: spec.from[from]}) 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 */ /* And add conversions from this type */
for (const to in this.Types) { for (const to in this.Types) {
if (type in (this.Types[to].from || {})) { if (type in (this.Types[to].from || {})) {
this._typed.addConversion( if (spec.refines == to || spec.refines in this._subtypes[to]) {
{from: type, to, convert: this.Types[to].from[type]}) 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: // rebundle anything that uses the new type:
this._invalidateDependents(':' + type) this._invalidateDependents(':' + type)
} }

View File

@ -1,7 +1,10 @@
export * from './Types/generic.mjs' export * from './Types/generic.mjs'
export {lcm} from './lcm.mjs'
export {mod} from './mod.mjs'
export {multiply} from './multiply.mjs' export {multiply} from './multiply.mjs'
export {divide} from './divide.mjs' export {divide} from './divide.mjs'
export {sign} from './sign.mjs' export {sign} from './sign.mjs'
export {sqrt} from './sqrt.mjs' export {sqrt} from './sqrt.mjs'
export {square} from './square.mjs'
export {subtract} from './subtract.mjs' export {subtract} from './subtract.mjs'

18
src/generic/gcdType.mjs Normal file
View File

@ -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
}

6
src/generic/lcm.mjs Normal file
View File

@ -0,0 +1,6 @@
export const lcm = {
'any,any': ({
multiply,
quotient,
gcd}) => (a,b) => multiply(quotient(a, gcd(a,b)), b)
}

6
src/generic/mod.mjs Normal file
View File

@ -0,0 +1,6 @@
export const mod = {
'any,any': ({
subtract,
multiply,
quotient}) => (a,m) => subtract(a, multiply(m, quotient(a,m)))
}

View File

@ -4,9 +4,9 @@ export const multiply = {
'undefined': () => u => u, 'undefined': () => u => u,
'undefined,...any': () => (u, rest) => u, 'undefined,...any': () => (u, rest) => u,
'any,undefined': () => (x, u) => u, 'any,undefined': () => (x, u) => u,
'any,undefined,...any': () => (x, u, rest) => u, 'any,any,...any': ({self}) => (a,b,rest) => {
'any,any,undefined': () => (x, y, u) => u, const later = [b, ...rest]
'any,any,undefined,...any': () => (x, y, u, rest) => u return later.reduce((x,y) => self(x,y), a)
// Bit of a hack since this should go on indefinitely... }
} }

3
src/generic/square.mjs Normal file
View File

@ -0,0 +1,3 @@
export const square = {
any: ({multiply}) => x => multiply(x,x)
}

View File

@ -5,4 +5,9 @@ Number.installType('number', {
test: n => typeof n === 'number', test: n => typeof n === 'number',
from: {string: s => +s} from: {string: s => +s}
}) })
Number.installType('NumInt', {
refines: 'number',
test: i => isFinite(i) && i === Math.round(i)
})
export {Number} export {Number}

3
src/number/isZero.mjs Normal file
View File

@ -0,0 +1,3 @@
export * from './Types/number.mjs'
export const isZero = {number: () => n => n === 0}

View File

@ -1,5 +1,3 @@
export * from './Types/number.mjs' export * from './Types/number.mjs'
export const multiply = { export const multiply = {'number,number': () => (m,n) => m*n}
'...number': () => multiplicands => multiplicands.reduce((x,y) => x*y, 1),
}

View File

@ -1,10 +1,16 @@
import gcdType from '../generic/gcdType.mjs'
export * from './Types/number.mjs' export * from './Types/number.mjs'
export {abs} from './abs.mjs' export {abs} from './abs.mjs'
export {add} from './add.mjs' export {add} from './add.mjs'
export const gcd = gcdType('NumInt')
export {invert} from './invert.mjs' export {invert} from './invert.mjs'
export {isZero} from './isZero.mjs'
export {multiply} from './multiply.mjs' export {multiply} from './multiply.mjs'
export {negate} from './negate.mjs' export {negate} from './negate.mjs'
export {one} from './one.mjs' export {one} from './one.mjs'
export {quotient} from './quotient.mjs'
export {roundquotient} from './roundquotient.mjs'
export {sqrt} from './sqrt.mjs' export {sqrt} from './sqrt.mjs'
export {zero} from './zero.mjs' export {zero} from './zero.mjs'

8
src/number/quotient.mjs Normal file
View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -52,4 +52,16 @@ describe('bigint', () => {
assert.deepStrictEqual(bo.sqrt(-3249n), bo.complex(0n, 57n)) 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)
})
}) })

View File

@ -29,4 +29,10 @@ describe('complex', () => {
math.complex(ms.negate(ms.sqrt(0.5)), ms.sqrt(0.5))) 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))
})
}) })

View File

@ -39,6 +39,7 @@ describe('A custom instance', () => {
assert.strictEqual(pm.subtract(5, 10), -5) assert.strictEqual(pm.subtract(5, 10), -5)
pm.install(complexAdd) pm.install(complexAdd)
pm.install(complexNegate) pm.install(complexNegate)
pm.install(complexComplex)
// Should be enough to allow complex subtraction, as subtract is generic: // Should be enough to allow complex subtraction, as subtract is generic:
assert.deepStrictEqual( assert.deepStrictEqual(
pm.subtract({re:5, im:0}, {re:10, im:1}), {re:-5, im: -1}) pm.subtract({re:5, im:0}, {re:10, im:1}), {re:-5, im: -1})

View File

@ -27,4 +27,7 @@ describe('number', () => {
assert.deepStrictEqual(no.sqrt(-16), no.complex(0,4)) assert.deepStrictEqual(no.sqrt(-16), no.complex(0,4))
}) })
it('computes gcd', () => {
assert.strictEqual(math.gcd(15, 35), 5)
})
}) })