From 883a351aa817ea26e4da5f4be1adc43ff04dcadf Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sat, 30 Jul 2022 09:40:47 -0700 Subject: [PATCH 01/10] refactor: Add stubs for function templates The stubs are so far all no-ops so basically at the moment the only allowed template parameter 'T' is just a synonym for 'any'. But at least this allows us to put the definition of the generic `subtract` into the exact form we actually want to support. --- src/core/PocomathInstance.mjs | 30 ++++++++++++++++++++++++++---- src/generic/subtract.mjs | 2 +- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index a6a2000..b1b865d 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -26,7 +26,10 @@ 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, T: anySpec} 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 @@ -130,6 +133,7 @@ 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) @@ -200,6 +204,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}`) @@ -295,10 +303,13 @@ export default class PocomathInstance { opImps[signature] = behavior 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 +318,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 === 'T' } + _addAffect(dependency, dependent) { if (dependency in this._affects) { this._affects[dependency].add(dependent) @@ -366,7 +383,9 @@ export default class PocomathInstance { } Object.defineProperty(this, name, {configurable: true, value: 'limbo'}) const tf_imps = {} - for (const [signature, {uses, does}] of usableEntries) { + for (const [rawSignature, {uses, does}] of usableEntries) { + /* For now, replace 'T' with 'any' */ + const signature = rawSignature.replaceAll('T', 'any') if (uses.length === 0) { tf_imps[signature] = does() } else { @@ -374,7 +393,10 @@ export default class PocomathInstance { let full_self_referential = false let part_self_references = [] for (const dep of uses) { - const [func, needsig] = dep.split(/[()]/) + let [func, needsig] = dep.split(/[()]/) + const needTypes = needsig ? typesOfSignature(needsig) : new Set() + /* For now, punt on template parameters */ + if (needTypes.has('T')) needsig = '' if (func === 'self') { if (needsig) { if (full_self_referential) { diff --git a/src/generic/subtract.mjs b/src/generic/subtract.mjs index 96a27bf..1c93b39 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)': addT, 'negate(T)': negT}) => (x,y) => addT(x, negT(y)) } From d4a4d8f331b25a8b1281a353c1b997a5374bb0d4 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sun, 31 Jul 2022 00:17:36 -0700 Subject: [PATCH 02/10] refactor: track all the 'prior types' of a type as types are installed These are exactly the types that might match before the given type, and hence which require to have their template instantiations defined when an instantiation with the given type is made. --- src/core/PocomathInstance.mjs | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index b1b865d..8cb572b 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -31,6 +31,13 @@ export default class PocomathInstance { */ this.Types = {any: anySpec, T: 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} @@ -170,7 +177,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) @@ -235,6 +247,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) { @@ -243,6 +256,7 @@ export default class PocomathInstance { while (nextSuper) { this._typed.addConversion( {from, to: nextSuper, convert: spec.from[from]}) + this._priorTypes[nextSuper].add(from) nextSuper = this.Types[nextSuper].refines } } @@ -261,21 +275,24 @@ export default class PocomathInstance { to: nextSuper, convert: this.Types[to].from[type] }) + 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._priorTypes[nextSuper].add(type) this._subtypes[nextSuper].add(type) nextSuper = this.Types[nextSuper].refines } + // rebundle anything that uses the new type: this._invalidateDependents(':' + type) } From 5c3716ff99b161b7560e1eb01ad2c585af5fbcf1 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sun, 31 Jul 2022 09:13:41 -0700 Subject: [PATCH 03/10] refactor: Avoid use of a "magic string " 'T' --- src/core/PocomathInstance.mjs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index 8cb572b..901514b 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -5,6 +5,8 @@ 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 + 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 @@ -339,7 +341,7 @@ export default class PocomathInstance { * 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 === 'T' } + _templateParam(t) { return t === theTemplateParam } _addAffect(dependency, dependent) { if (dependency in this._affects) { @@ -401,8 +403,8 @@ export default class PocomathInstance { Object.defineProperty(this, name, {configurable: true, value: 'limbo'}) const tf_imps = {} for (const [rawSignature, {uses, does}] of usableEntries) { - /* For now, replace 'T' with 'any' */ - const signature = rawSignature.replaceAll('T', 'any') + /* For now, replace theTemplateParam with 'any' */ + const signature = rawSignature.replaceAll(theTemplateParam, 'any') if (uses.length === 0) { tf_imps[signature] = does() } else { @@ -413,7 +415,7 @@ export default class PocomathInstance { let [func, needsig] = dep.split(/[()]/) const needTypes = needsig ? typesOfSignature(needsig) : new Set() /* For now, punt on template parameters */ - if (needTypes.has('T')) needsig = '' + if (needTypes.has(theTemplateParam)) needsig = '' if (func === 'self') { if (needsig) { if (full_self_referential) { From 0fbcbf661e18eb425ea9699f4c3145ddda444025 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sun, 31 Jul 2022 09:36:45 -0700 Subject: [PATCH 04/10] refactor: separate the generation of a typed-function implementation --- src/core/PocomathInstance.mjs | 149 ++++++++++++++++++---------------- 1 file changed, 80 insertions(+), 69 deletions(-) diff --git a/src/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index 901514b..0a589ca 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -319,7 +319,8 @@ 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' || this._templateParam(depname)) { @@ -402,80 +403,90 @@ export default class PocomathInstance { } Object.defineProperty(this, name, {configurable: true, value: 'limbo'}) const tf_imps = {} - for (const [rawSignature, {uses, does}] of usableEntries) { + for (const [rawSignature, behavior] of usableEntries) { /* For now, replace theTemplateParam with 'any' */ const signature = rawSignature.replaceAll(theTemplateParam, 'any') - 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) { - 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) { - 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) - } - } + this._addTFimplementation( + tf_imps, signature, behavior.uses, behavior.does) } 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, uses, does) { + 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) + } } From 5cea71cb254b246ba15a5d651c2f04b229bc09bc Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sun, 31 Jul 2022 09:44:36 -0700 Subject: [PATCH 05/10] refactor: isolate in the code the point of template instantiation --- src/core/PocomathInstance.mjs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index 0a589ca..62c6757 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -404,7 +404,21 @@ export default class PocomathInstance { Object.defineProperty(this, name, {configurable: true, value: 'limbo'}) const tf_imps = {} for (const [rawSignature, behavior] of usableEntries) { - /* For now, replace theTemplateParam with 'any' */ + /* 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.uses, behavior.does) + continue + } + /* It's a template, have to instantiate */ + /* But just for initial testing, punt and use T as synonym for 'any' */ const signature = rawSignature.replaceAll(theTemplateParam, 'any') this._addTFimplementation( tf_imps, signature, behavior.uses, behavior.does) From 171b6941f42f680062e0531a3503f8e155817ed8 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sun, 31 Jul 2022 10:01:20 -0700 Subject: [PATCH 06/10] refactor: add stub for instantiating the template --- src/core/PocomathInstance.mjs | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index 62c6757..4fc7b5b 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -7,6 +7,14 @@ 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 @@ -413,15 +421,22 @@ export default class PocomathInstance { } } if (explicit) { - this._addTFimplementation( - tf_imps, rawSignature, behavior.uses, behavior.does) + this._addTFimplementation(tf_imps, rawSignature, behavior) continue } /* It's a template, have to instantiate */ - /* But just for initial testing, punt and use T as synonym for 'any' */ - const signature = rawSignature.replaceAll(theTemplateParam, 'any') - this._addTFimplementation( - tf_imps, signature, behavior.uses, behavior.does) + /* First, add the known instantiations */ + if (!('instantiations' in behavior)) { + behavior.instantiations = new Set() + } + for (const instType of behavior.instantiations) { + const signature = + substituteInSig(rawSignature, theTemplateParam, instType) + this._addTFimplementation(tf_imps, signature, behavior, instType) + } + /* Now add the catchall signature; for now, punting on the behavior */ + const signature = substituteInSig(rawSignature, theTemplateParam, 'any') + this._addTFimplementation(tf_imps, signature, behavior) } const tf = this._typed(name, tf_imps) Object.defineProperty(this, name, {configurable: true, value: tf}) @@ -432,7 +447,8 @@ export default class PocomathInstance { * to typed-function implementations and inserts the result into plain object * imps */ - _addTFimplementation(imps, signature, uses, does) { + _addTFimplementation(imps, signature, behavior, instType) { + const {uses, does} = behavior if (uses.length === 0) { imps[signature] = does() return From 7bbdc049ce20f62f0da1858a2d9ed590e8c66e6a Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sun, 31 Jul 2022 21:08:28 -0700 Subject: [PATCH 07/10] refactor: add stub for patch to generic template behavior --- src/core/PocomathInstance.mjs | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index 4fc7b5b..ba502d0 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -434,9 +434,35 @@ export default class PocomathInstance { substituteInSig(rawSignature, theTemplateParam, instType) this._addTFimplementation(tf_imps, signature, behavior, instType) } - /* Now add the catchall signature; for now, punting on the behavior */ + /* Now add the catchall signature */ const signature = substituteInSig(rawSignature, theTemplateParam, 'any') - this._addTFimplementation(tf_imps, signature, behavior) + /* The catchall signature has to detect the actual type of the call + * and add the new instantiations + */ + const argTypes = rawSignature.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) => { + const original = behavior.does(refs) + return (...args) => { + const example = args[exemplar] + console.log('Have to match template to', example) + return original(...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}) From 1076c3c7276cb3880fff3779fd43dd28fec14fd0 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sun, 31 Jul 2022 21:26:16 -0700 Subject: [PATCH 08/10] feat: add built-in typeOf operator to PocomathInstance --- src/core/PocomathInstance.mjs | 13 ++++++++++++- test/_pocomath.mjs | 9 +++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index ba502d0..72bfc20 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -26,6 +26,7 @@ export default class PocomathInstance { 'install', 'installType', 'name', + 'typeOf', 'Types', 'undefinedTypes' ]) @@ -153,7 +154,13 @@ export default class PocomathInstance { 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) } /** @@ -305,6 +312,10 @@ export default class PocomathInstance { // 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 diff --git a/test/_pocomath.mjs b/test/_pocomath.mjs index 8e88668..3e9f0a0 100644 --- a/test/_pocomath.mjs +++ b/test/_pocomath.mjs @@ -10,6 +10,15 @@ 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) }) From fd3d6b2eb3c58c6b3b22f948107cc8c796602003 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sun, 31 Jul 2022 23:33:58 -0700 Subject: [PATCH 09/10] feat: Template implementations --- src/bigint/add.mjs | 4 +-- src/complex/add.mjs | 14 +++++----- src/core/PocomathInstance.mjs | 43 +++++++++++++++++++++++++------ src/generic/arithmetic.mjs | 5 +++- src/generic/reducingOperation.mjs | 12 +++++++++ src/generic/subtract.mjs | 2 +- src/number/add.mjs | 4 +-- test/_pocomath.mjs | 4 ++- test/custom.mjs | 2 ++ 9 files changed, 67 insertions(+), 23 deletions(-) create mode 100644 src/generic/reducingOperation.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/complex/add.mjs b/src/complex/add.mjs index 58d9eea..8f26167 100644 --- a/src/complex/add.mjs +++ b/src/complex/add.mjs @@ -1,10 +1,12 @@ 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) - } + 'Complex,number': ({ + 'self(number,number)': addNum, + 'complex(any,any)': cplx + }) => (z,x) => cplx(addNum(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/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index 72bfc20..6ef3288 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -443,7 +443,26 @@ export default class PocomathInstance { for (const instType of behavior.instantiations) { const signature = substituteInSig(rawSignature, theTemplateParam, instType) - this._addTFimplementation(tf_imps, signature, behavior, instType) + 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(rawSignature, theTemplateParam, 'any') @@ -464,13 +483,21 @@ export default class PocomathInstance { `Cannot find template parameter in ${rawSignature}`) } const self = this - const patch = (refs) => { - const original = behavior.does(refs) - return (...args) => { - const example = args[exemplar] - console.log('Have to match template to', example) - return original(...args) + const patch = (refs) => (...args) => { + 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) + } + // 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}) @@ -484,7 +511,7 @@ export default class PocomathInstance { * to typed-function implementations and inserts the result into plain object * imps */ - _addTFimplementation(imps, signature, behavior, instType) { + _addTFimplementation(imps, signature, behavior) { const {uses, does} = behavior if (uses.length === 0) { imps[signature] = does() 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/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/subtract.mjs b/src/generic/subtract.mjs index 1c93b39..b048d0c 100644 --- a/src/generic/subtract.mjs +++ b/src/generic/subtract.mjs @@ -1,3 +1,3 @@ export const subtract = { - 'T,T': ({'add(T)': addT, 'negate(T)': negT}) => (x,y) => addT(x, negT(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/test/_pocomath.mjs b/test/_pocomath.mjs index 3e9f0a0..b17ec5c 100644 --- a/test/_pocomath.mjs +++ b/test/_pocomath.mjs @@ -21,12 +21,14 @@ describe('The default full pocomath instance "math"', () => { 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', () => { @@ -55,10 +57,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/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: */ From 36e7b750ce9757397c8ad50fc65af6ebc8907e3f Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Mon, 1 Aug 2022 02:57:38 -0700 Subject: [PATCH 10/10] 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. --- src/bigint/all.mjs | 2 +- src/bigint/compare.mjs | 5 +++ src/bigint/native.mjs | 1 + src/complex/add.mjs | 10 ++++++ src/complex/equal.mjs | 19 +++++++++++ src/complex/native.mjs | 1 + src/complex/sqrt.mjs | 12 ++----- src/core/PocomathInstance.mjs | 60 +++++++++++++++++++++++++---------- src/generic/all.mjs | 1 + 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/relational.mjs | 54 +++++++++++++++++++++++++++++++ src/generic/sign.mjs | 5 +-- src/generic/square.mjs | 2 +- src/number/all.mjs | 2 +- src/number/compare.mjs | 52 ++++++++++++++++++++++++++++++ src/number/native.mjs | 1 + test/_pocomath.mjs | 8 +++-- test/complex/_all.mjs | 7 ++++ test/number/_all.mjs | 7 ++++ 23 files changed, 233 insertions(+), 44 deletions(-) create mode 100644 src/bigint/compare.mjs create mode 100644 src/complex/equal.mjs create mode 100644 src/generic/relational.mjs create mode 100644 src/number/compare.mjs 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 8f26167..1afd22f 100644 --- a/src/complex/add.mjs +++ b/src/complex/add.mjs @@ -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 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 6ef3288..180ca17 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -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) 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/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/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/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 b17ec5c..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"', () => { @@ -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', () => { 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/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))) + }) + })