From fe54bc60048a28d83ac06527201ec3976130440d Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Mon, 1 Aug 2022 10:09:32 +0000 Subject: [PATCH] feat: Template operations (#41) Relational functions are added using templates, and existing generic functions are made more strict with them. Also a new built-in typeOf function is added, that automatically updates itself. Resolves #34. Co-authored-by: Glen Whitney Reviewed-on: https://code.studioinfinity.org/glen/pocomath/pulls/41 --- src/bigint/add.mjs | 4 +- src/bigint/all.mjs | 2 +- src/bigint/compare.mjs | 5 + src/bigint/native.mjs | 1 + src/complex/add.mjs | 24 ++- src/complex/equal.mjs | 19 ++ src/complex/native.mjs | 1 + src/complex/sqrt.mjs | 12 +- src/core/PocomathInstance.mjs | 326 +++++++++++++++++++++++------- src/generic/all.mjs | 1 + src/generic/arithmetic.mjs | 5 +- 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/reducingOperation.mjs | 12 ++ src/generic/relational.mjs | 54 +++++ src/generic/sign.mjs | 5 +- src/generic/square.mjs | 2 +- src/generic/subtract.mjs | 2 +- src/number/add.mjs | 4 +- src/number/all.mjs | 2 +- src/number/compare.mjs | 52 +++++ src/number/native.mjs | 1 + test/_pocomath.mjs | 21 +- test/complex/_all.mjs | 7 + test/custom.mjs | 2 + test/number/_all.mjs | 7 + 29 files changed, 480 insertions(+), 119 deletions(-) create mode 100644 src/bigint/compare.mjs create mode 100644 src/complex/equal.mjs create mode 100644 src/generic/reducingOperation.mjs create mode 100644 src/generic/relational.mjs create mode 100644 src/number/compare.mjs diff --git a/src/bigint/add.mjs b/src/bigint/add.mjs index 31e2926..1cd296d 100644 --- a/src/bigint/add.mjs +++ b/src/bigint/add.mjs @@ -1,5 +1,3 @@ export * from './Types/bigint.mjs' -export const add = { - '...bigint': () => addends => addends.reduce((x,y) => x+y, 0n) -} +export const add = {'bigint,bigint': () => (a,b) => a+b} 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 58d9eea..1afd22f 100644 --- a/src/complex/add.mjs +++ b/src/complex/add.mjs @@ -1,10 +1,22 @@ export * from './Types/Complex.mjs' export const add = { - '...Complex': ({self}) => addends => { - if (addends.length === 0) return {re:0, im:0} - const seed = addends.shift() - return addends.reduce( - (w,z) => ({re: self(w.re, z.re), im: self(w.im, z.im)}), seed) - } + /* 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 + }) => (w,z) => cplx(self(w.re, z.re), self(w.im, z.im)) } 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 a6a2000..180ca17 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -5,6 +5,16 @@ import {subsetOfKeys, typesOfSignature} from './utils.mjs' const anySpec = {} // fixed dummy specification of 'any' type +const theTemplateParam = 'T' // First pass: only allow this one exact parameter + +/* Returns a new signature just like sig but with the parameter replaced by + * the type + */ +function substituteInSig(sig, parameter, type) { + const pattern = new RegExp("\\b" + parameter + "\\b", 'g') + return sig.replaceAll(pattern, type) +} + export default class PocomathInstance { /* Disallowed names for ops; beware, this is slightly non-DRY * in that if a new top-level PocomathInstance method is added, its name @@ -16,6 +26,7 @@ export default class PocomathInstance { 'install', 'installType', 'name', + 'typeOf', 'Types', 'undefinedTypes' ]) @@ -26,11 +37,22 @@ export default class PocomathInstance { this._affects = {} this._typed = typed.create() this._typed.clear() - this.Types = {any: anySpec} // dummy entry to track the default 'any' type + /* List of types installed in the instance. We start with just dummies + * for the 'any' type and for type parameters: + */ + 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. + * This is important because if we instantiate a template, we must + * instantiate it for all prior types as well, or else the wrong instance + * might match. + */ + 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], @@ -85,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) { @@ -130,9 +166,16 @@ export default class PocomathInstance { _installInstance(other) { for (const [type, spec] of Object.entries(other.Types)) { + if (type === 'any' || this._templateParam(type)) continue this.installType(type, spec) } - this._installFunctions(other._imps) + const migrateImps = {} + for (const operator in other._imps) { + if (operator != 'typeOf') { // skip the builtin, we already have it + migrateImps[operator] = other._imps[operator] + } + } + this._installFunctions(migrateImps) } /** @@ -166,7 +209,12 @@ export default class PocomathInstance { const mod = await import(modName) this.install(mod) } catch (err) { - // No such module, but that's OK + if (!(err.message.includes('find'))) { + // Not just a error because module doesn't exist + // So take it seriously + throw err + } + // We don't care if a module doesn't exist, so merely proceed } } doneSet.add(name) @@ -200,6 +248,10 @@ export default class PocomathInstance { * the corresponding changes to the _typed object immediately */ installType(type, spec) { + if (this._templateParam(type)) { + throw new SyntaxError( + `Type name '${type}' reserved for template parameter`) + } if (type in this.Types) { if (spec !== this.Types[type]) { throw new SyntaxError(`Conflicting definitions of type ${type}`) @@ -227,6 +279,7 @@ export default class PocomathInstance { } this._typed.addTypes([{name: type, test: testFn}], beforeType) this.Types[type] = spec + this._priorTypes[type] = new Set() /* Now add conversions to this type */ for (const from in (spec.from || {})) { if (from in this.Types) { @@ -235,6 +288,8 @@ 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 } } @@ -253,23 +308,30 @@ 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 } } } - if (spec.refines) { - this._typed.addConversion( - {from: type, to: spec.refines, convert: x => x}) - } + // Update all the subtype sets of supertypes up the chain, and + // while we are at it add trivial conversions from subtypes to supertypes + // to help typed-function match signatures properly: this._subtypes[type] = new Set() - // Update all the subtype sets of supertypes up the chain: let nextSuper = spec.refines 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} + this._installFunctions({typeOf: imp}) } /* Returns a list of all types that have been mentioned in the @@ -292,13 +354,17 @@ export default class PocomathInstance { `Conflicting definitions of ${signature} for ${name}`) } } else { - opImps[signature] = behavior + // Must avoid aliasing into another instance: + opImps[signature] = {uses: behavior.uses, does: behavior.does} for (const dep of behavior.uses) { const depname = dep.split('(', 1)[0] - if (depname === 'self') continue + if (depname === 'self' || this._templateParam(depname)) { + continue + } this._addAffect(depname, name) } for (const type of typesOfSignature(signature)) { + if (this._templateParam(type)) continue this._usedTypes.add(type) this._addAffect(':' + type, name) } @@ -307,6 +373,12 @@ export default class PocomathInstance { } } + /* returns a boolean indicating whether t denotes a template parameter. + * We will start this out very simply: the special string `T` is always + * a template parameter, and that's the only one + */ + _templateParam(t) { return t === theTemplateParam } + _addAffect(dependency, dependent) { if (dependency in this._affects) { this._affects[dependency].add(dependent) @@ -366,75 +438,177 @@ export default class PocomathInstance { } Object.defineProperty(this, name, {configurable: true, value: 'limbo'}) const tf_imps = {} - for (const [signature, {uses, does}] of usableEntries) { - if (uses.length === 0) { - tf_imps[signature] = does() - } else { - const refs = {} - let full_self_referential = false - let part_self_references = [] - for (const dep of uses) { - const [func, needsig] = dep.split(/[()]/) - if (func === 'self') { - if (needsig) { - if (full_self_referential) { - throw new SyntaxError( - 'typed-function does not support mixed full and ' - + 'partial self-reference') - } - if (subsetOfKeys(typesOfSignature(needsig), this.Types)) { - part_self_references.push(needsig) - } - } else { - if (part_self_references.length) { - throw new SyntaxError( - 'typed-function does not support mixed full and ' - + 'partial self-reference') - } - full_self_referential = true - } - } else { - if (this[func] === 'limbo') { - /* We are in the midst of bundling func, so have to use - * an indirect reference to func. And given that, there's - * really no helpful way to extract a specific signature - */ - const self = this - refs[dep] = function () { // is this the most efficient? - return self[func].apply(this, arguments) - } - } else { - // can bundle up func, and grab its signature if need be - let destination = this[func] - if (needsig) { - destination = this._typed.find(destination, needsig) - } - refs[dep] = destination - } - } - } - if (full_self_referential) { - tf_imps[signature] = this._typed.referToSelf(self => { - refs.self = self - return does(refs) - }) - } else if (part_self_references.length) { - tf_imps[signature] = this._typed.referTo( - ...part_self_references, (...impls) => { - for (let i = 0; i < part_self_references.length; ++i) { - refs[`self(${part_self_references[i]})`] = impls[i] - } - return does(refs) - } - ) - } else { - tf_imps[signature] = does(refs) + for (const [rawSignature, behavior] of usableEntries) { + /* Check if it's an ordinary non-template signature */ + let explicit = true + for (const type of typesOfSignature(rawSignature)) { + if (this._templateParam(type)) { // template types need better check + explicit = false + break } } + if (explicit) { + this._addTFimplementation(tf_imps, rawSignature, behavior) + continue + } + /* It's a template, have to instantiate */ + /* First, add the known instantiations, gathering all types needed */ + if (!('instantiations' in behavior)) { + behavior.instantiations = new Set() + } + 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(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 + uses.add(substituteInSig(dep, theTemplateParam, instType)) + } + const patch = (refs) => { + const innerRefs = {} + for (const dep of behavior.uses) { + if (this._templateParam(dep)) { + innerRefs[dep] = instType + } else { + const outerName = substituteInSig( + dep, theTemplateParam, instType) + innerRefs[dep] = refs[outerName] + } + } + const original = behavior.does(innerRefs) + return behavior.does(innerRefs) + } + this._addTFimplementation(tf_imps, signature, {uses, does: patch}) + } + /* Now add the catchall signature */ + 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 = trimSignature.split(',') + let exemplar = -1 + for (let i = 0; i < argTypes.length; ++i) { + const argType = argTypes[i].trim() + if (argType === theTemplateParam) { + exemplar = i + break + } + } + if (exemplar < 0) { + throw new SyntaxError( + `Cannot find template parameter in ${rawSignature}`) + } + const self = this + const patch = (refs) => (...args) => { + const example = args[exemplar] + const instantiateFor = self.typeOf(example) + refs[theTemplateParam] = instantiateFor + behavior.instantiations.add(instantiateFor) + 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) + } + this._addTFimplementation( + tf_imps, signature, {uses: behavior.uses, does: patch}) } const tf = this._typed(name, tf_imps) Object.defineProperty(this, name, {configurable: true, value: tf}) return tf } + /* Adapts Pocomath-style behavior specification (uses, does) for signature + * to typed-function implementations and inserts the result into plain object + * imps + */ + _addTFimplementation(imps, signature, behavior) { + const {uses, does} = behavior + if (uses.length === 0) { + imps[signature] = does() + return + } + const refs = {} + let full_self_referential = false + let part_self_references = [] + for (const dep of uses) { + let [func, needsig] = dep.split(/[()]/) + const needTypes = needsig ? typesOfSignature(needsig) : new Set() + /* For now, punt on template parameters */ + if (needTypes.has(theTemplateParam)) needsig = '' + if (func === 'self') { + if (needsig) { + if (full_self_referential) { + throw new SyntaxError( + 'typed-function does not support mixed full and ' + + 'partial self-reference') + } + if (subsetOfKeys(typesOfSignature(needsig), this.Types)) { + part_self_references.push(needsig) + } + } else { + if (part_self_references.length) { + throw new SyntaxError( + 'typed-function does not support mixed full and ' + + 'partial self-reference') + } + full_self_referential = true + } + } else { + if (this[func] === 'limbo') { + /* We are in the midst of bundling func, so have to use + * an indirect reference to func. And given that, there's + * really no helpful way to extract a specific signature + */ + const self = this + refs[dep] = function () { // is this the most efficient? + return self[func].apply(this, arguments) + } + } else { + // can bundle up func, and grab its signature if need be + let destination = this[func] + if (needsig) { + destination = this._typed.find(destination, needsig) + } + refs[dep] = destination + } + } + } + if (full_self_referential) { + imps[signature] = this._typed.referToSelf(self => { + refs.self = self + return does(refs) + }) + return + } + if (part_self_references.length) { + imps[signature] = this._typed.referTo( + ...part_self_references, (...impls) => { + for (let i = 0; i < part_self_references.length; ++i) { + refs[`self(${part_self_references[i]})`] = impls[i] + } + return does(refs) + } + ) + return + } + imps[signature] = does(refs) + } } 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/arithmetic.mjs b/src/generic/arithmetic.mjs index 8bcc904..7576dbe 100644 --- a/src/generic/arithmetic.mjs +++ b/src/generic/arithmetic.mjs @@ -1,8 +1,11 @@ +import {reducingOperation} from './reducingOperation.mjs' + export * from './Types/generic.mjs' +export const add = reducingOperation export {lcm} from './lcm.mjs' export {mod} from './mod.mjs' -export {multiply} from './multiply.mjs' +export const multiply = reducingOperation export {divide} from './divide.mjs' export {sign} from './sign.mjs' export {sqrt} from './sqrt.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/reducingOperation.mjs b/src/generic/reducingOperation.mjs new file mode 100644 index 0000000..101a8ec --- /dev/null +++ b/src/generic/reducingOperation.mjs @@ -0,0 +1,12 @@ +export * from './Types/generic.mjs' + +export const reducingOperation = { + 'undefined': () => u => u, + 'undefined,...any': () => (u, rest) => u, + 'any,undefined': () => (x, u) => u, + any: () => x => x, + 'any,any,...any': ({ + self + }) => (a,b,rest) => [b, ...rest].reduce((x,y) => self(x,y), a) +} + 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/generic/subtract.mjs b/src/generic/subtract.mjs index 96a27bf..b048d0c 100644 --- a/src/generic/subtract.mjs +++ b/src/generic/subtract.mjs @@ -1,3 +1,3 @@ export const subtract = { - 'any,any': ({add, negate}) => (x,y) => add(x, negate(y)) + 'T,T': ({'add(T,T)': addT, 'negate(T)': negT}) => (x,y) => addT(x, negT(y)) } diff --git a/src/number/add.mjs b/src/number/add.mjs index e6610ee..7d79637 100644 --- a/src/number/add.mjs +++ b/src/number/add.mjs @@ -1,5 +1,3 @@ export * from './Types/number.mjs' -export const add = { - '...number': () => addends => addends.reduce((x,y) => x+y, 0), -} +export const add = {'number,number': () => (m,n) => m+n} 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 8e88668..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"', () => { @@ -10,14 +11,25 @@ describe('The default full pocomath instance "math"', () => { assert.strictEqual(undef.length, 0) }) + it('has a built-in typeOf operator', () => { + assert.strictEqual(math.typeOf(42), 'NumInt') + assert.strictEqual(math.typeOf(-1.5), 'number') + assert.strictEqual(math.typeOf(-42n), 'bigint') + assert.strictEqual(math.typeOf(undefined), 'undefined') + assert.strictEqual(math.typeOf({re: 15n, im: -2n}), 'GaussianInteger') + assert.strictEqual(math.typeOf({re: 6.28, im: 2.72}), 'Complex') + }) + it('can subtract numbers', () => { assert.strictEqual(math.subtract(12, 5), 7) + //assert.strictEqual(math.subtract(3n, 1.5), 1.5) }) it('can add numbers', () => { assert.strictEqual(math.add(3, 4), 7) assert.strictEqual(math.add(1.5, 2.5, 3.5), 7.5) assert.strictEqual(math.add(Infinity), Infinity) + assert.throws(() => math.add(3n, -1.5), TypeError) }) it('can negate numbers', () => { @@ -26,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', () => { @@ -46,10 +59,10 @@ describe('The default full pocomath instance "math"', () => { assert.deepStrictEqual( math.subtract(math.complex(1,1), math.complex(2,-2)), math.complex(-1,3)) + assert.strictEqual(math.negate(math.complex(3, 8)).im, -8) assert.deepStrictEqual( math.subtract(16, math.add(3, math.complex(0,4), 2)), math.complex(11, -4)) - assert.strictEqual(math.negate(math.complex(3, 8)).im, -8) }) it('handles bigints', () => { 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/custom.mjs b/test/custom.mjs index ab26563..7aef959 100644 --- a/test/custom.mjs +++ b/test/custom.mjs @@ -3,6 +3,7 @@ import math from '../src/pocomath.mjs' import PocomathInstance from '../src/core/PocomathInstance.mjs' import * as numbers from '../src/number/all.mjs' import * as numberAdd from '../src/number/add.mjs' +import {add as genericAdd} from '../src/generic/arithmetic.mjs' import * as complex from '../src/complex/all.mjs' import * as complexAdd from '../src/complex/add.mjs' import * as complexNegate from '../src/complex/negate.mjs' @@ -66,6 +67,7 @@ describe('A custom instance', () => { const cherry = new PocomathInstance('cherry') cherry.install(numberAdd) await extendToComplex(cherry) + cherry.install({add: genericAdd}) /* Now we have an instance that supports addition for number and complex and little else: */ 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))) + }) + })