Merge pull request 'feat: Implement subtypes' (#37) from subtypes into main

Reviewed-on: #37
This commit is contained in:
Glen Whitney 2022-07-30 12:02:45 +00:00
commit 6775b66686
35 changed files with 294 additions and 43 deletions

View File

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

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 const multiply = {
'...bigint': () => multiplicands => multiplicands.reduce((x,y) => x*y, 1n)
}
export const multiply = {'bigint,bigint': () => (a,b) => a*b}

View File

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

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', {
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}

View File

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

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 {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'

View File

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

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

View File

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

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,...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)
}
}

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',
from: {string: s => +s}
})
Number.installType('NumInt', {
refines: 'number',
test: i => isFinite(i) && i === Math.round(i)
})
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 const multiply = {
'...number': () => multiplicands => multiplicands.reduce((x,y) => x*y, 1),
}
export const multiply = {'number,number': () => (m,n) => m*n}

View File

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

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

View File

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