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.
This commit is contained in:
Glen Whitney 2022-08-01 02:57:38 -07:00
parent fd3d6b2eb3
commit 36e7b750ce
23 changed files with 233 additions and 44 deletions

View File

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

5
src/bigint/compare.mjs Normal file
View File

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

View File

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

View File

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

19
src/complex/equal.mjs Normal file
View File

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

View File

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

View File

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

View File

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

View File

@ -1 +1,2 @@
export * from './arithmetic.mjs'
export * from './relational.mjs'

View File

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

View File

@ -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 => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

52
src/number/compare.mjs Normal file
View File

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

View File

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

View File

@ -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', () => {

View File

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

View File

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