From 883a351aa817ea26e4da5f4be1adc43ff04dcadf Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sat, 30 Jul 2022 09:40:47 -0700 Subject: [PATCH 01/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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))) + }) + }) From fe54bc60048a28d83ac06527201ec3976130440d Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Mon, 1 Aug 2022 10:09:32 +0000 Subject: [PATCH 11/22] 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))) + }) + }) From 7d1a435aa06b6b7ac9273512ea41e5fd24c98be9 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Mon, 1 Aug 2022 08:28:21 -0700 Subject: [PATCH 12/22] feat(floor): Provide example of op-centric organization --- src/bigint/all.mjs | 3 ++- src/complex/all.mjs | 3 ++- src/core/PocomathInstance.mjs | 1 + src/number/all.mjs | 3 ++- src/ops/floor.mjs | 25 +++++++++++++++++++++++++ src/pocomath.mjs | 4 +++- test/_pocomath.mjs | 6 +++--- test/bigint/_all.mjs | 4 ++++ test/complex/_all.mjs | 8 ++++++++ test/custom.mjs | 10 +++++++++- test/number/_all.mjs | 6 ++++++ 11 files changed, 65 insertions(+), 8 deletions(-) create mode 100644 src/ops/floor.mjs diff --git a/src/bigint/all.mjs b/src/bigint/all.mjs index 74dce18..f9da9f0 100644 --- a/src/bigint/all.mjs +++ b/src/bigint/all.mjs @@ -1,5 +1,6 @@ import PocomathInstance from '../core/PocomathInstance.mjs' import * as bigints from './native.mjs' import * as generic from '../generic/all.mjs' +import * as floor from '../ops/floor.mjs' -export default PocomathInstance.merge('bigint', bigints, generic) +export default PocomathInstance.merge('bigint', bigints, generic, floor) diff --git a/src/complex/all.mjs b/src/complex/all.mjs index 5d9f3b3..98eae90 100644 --- a/src/complex/all.mjs +++ b/src/complex/all.mjs @@ -1,5 +1,6 @@ import PocomathInstance from '../core/PocomathInstance.mjs' import * as complexes from './native.mjs' import * as generic from '../generic/arithmetic.mjs' +import * as floor from '../ops/floor.mjs' -export default PocomathInstance.merge('complex', complexes, generic) +export default PocomathInstance.merge('complex', complexes, generic, floor) diff --git a/src/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index 180ca17..4858a54 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -471,6 +471,7 @@ export default class PocomathInstance { } for (const instType of instantiationSet) { + if (!(instType in this.Types)) continue if (this.Types[instType] === anySpec) continue const signature = substituteInSig(trimSignature, theTemplateParam, instType) diff --git a/src/number/all.mjs b/src/number/all.mjs index 4a1228e..bedaa24 100644 --- a/src/number/all.mjs +++ b/src/number/all.mjs @@ -1,6 +1,7 @@ import PocomathInstance from '../core/PocomathInstance.mjs' import * as numbers from './native.mjs' import * as generic from '../generic/all.mjs' +import * as floor from '../ops/floor.mjs' -export default PocomathInstance.merge('number', numbers, generic) +export default PocomathInstance.merge('number', numbers, generic, floor) diff --git a/src/ops/floor.mjs b/src/ops/floor.mjs new file mode 100644 index 0000000..98d524a --- /dev/null +++ b/src/ops/floor.mjs @@ -0,0 +1,25 @@ +import {Complex} from '../complex/Types/Complex.mjs' + +/* Note we don't **export** any types here, so that only the options + * below that correspond to types that have been installed are activated. + */ + +export const floor = { + bigint: () => x => x, + NumInt: () => x => x, // Because Pocomath isn't part of typed-function, or + GaussianInteger: () => x => x, // at least have access to the real + // typed-function parse, we unfortunately can't coalesce these into one + // entry with type `bigint|NumInt|GaussianInteger` because they couldn't + // be separately activated then + + number: ({'equal(number,number)': eq}) => n => { + if (eq(n, Math.round(n))) return Math.round(n) + return Math.floor(n) + }, + + Complex: Complex.promoteUnary.Complex, + + // OK to include a type totally not in Pocomath yet, it'll never be + // activated. + Fraction: ({quotient}) => f => quotient(f.n, f.d), +} diff --git a/src/pocomath.mjs b/src/pocomath.mjs index 65f8103..cb9bf94 100644 --- a/src/pocomath.mjs +++ b/src/pocomath.mjs @@ -4,7 +4,9 @@ import * as numbers from './number/native.mjs' import * as bigints from './bigint/native.mjs' import * as complex from './complex/native.mjs' import * as generic from './generic/all.mjs' +import * as floor from './ops/floor.mjs' -const math = PocomathInstance.merge('math', numbers, bigints, complex, generic) +const math = PocomathInstance.merge( + 'math', numbers, bigints, complex, generic, floor) export default math diff --git a/test/_pocomath.mjs b/test/_pocomath.mjs index 1a6fabe..3e6f2fe 100644 --- a/test/_pocomath.mjs +++ b/test/_pocomath.mjs @@ -3,12 +3,12 @@ import PocomathInstance from '../src/core/PocomathInstance.mjs' import math from '../src/pocomath.mjs' describe('The default full pocomath instance "math"', () => { - it('has no undefined types', () => { + it('has no unexpected undefined types', () => { const undef = math.undefinedTypes() if (undef.length) { - console.log('Probable typo: found undefined types', undef) + console.log('NOTE: Found undefined types', undef) } - assert.strictEqual(undef.length, 0) + assert.strictEqual(undef.length, 1) // Mentioning 'Fraction' but not using }) it('has a built-in typeOf operator', () => { diff --git a/test/bigint/_all.mjs b/test/bigint/_all.mjs index 29b3d27..371b2a0 100644 --- a/test/bigint/_all.mjs +++ b/test/bigint/_all.mjs @@ -64,4 +64,8 @@ describe('bigint', () => { assert.strictEqual(math.lcm(0n, 0n), 0n) }) + it('allows floor as a no-op', () => { + assert.strictEqual(math.floor(-333n), -333n) + }) + }) diff --git a/test/complex/_all.mjs b/test/complex/_all.mjs index ae146b6..e486fc3 100644 --- a/test/complex/_all.mjs +++ b/test/complex/_all.mjs @@ -51,4 +51,12 @@ describe('complex', () => { math.complex(4n, 5n)) }) + it('computes floor', () => { + assert.deepStrictEqual( + math.floor(math.complex(19, 22.7)), + math.complex(19, 22)) + const gi = math.complex(-1n, 1n) + assert.strictEqual(math.floor(gi), gi) // literally a no-op + }) + }) diff --git a/test/custom.mjs b/test/custom.mjs index 7aef959..ba4c713 100644 --- a/test/custom.mjs +++ b/test/custom.mjs @@ -38,12 +38,20 @@ describe('A custom instance', () => { const pm = new PocomathInstance('piecemeal') pm.install(numbers) assert.strictEqual(pm.subtract(5, 10), -5) + assert.strictEqual(pm.floor(3.7), 3) + assert.throws(() => pm.floor(10n), TypeError) pm.install(complexAdd) pm.install(complexNegate) pm.install(complexComplex) // Should be enough to allow complex subtraction, as subtract is generic: assert.deepStrictEqual( - pm.subtract({re:5, im:0}, {re:10, im:1}), {re:-5, im: -1}) + pm.subtract(pm.complex(5, 0), pm.complex(10, 1)), + math.complex(-5, -1)) + // And now floor has been activated for Complex as well, since the type + // is present + assert.deepStrictEqual( + pm.floor(math.complex(1.9, 0)), + math.complex(1)) }) it("can defer definition of (even used) types", () => { diff --git a/test/number/_all.mjs b/test/number/_all.mjs index f8e7312..49c7da6 100644 --- a/test/number/_all.mjs +++ b/test/number/_all.mjs @@ -37,4 +37,10 @@ describe('number', () => { assert.ok(math.largerEq(12.5, math.divide(25,2))) }) + it('Computes floor', () => { + assert.strictEqual(math.floor(7), 7) + assert.strictEqual(math.floor(6.99), 6) + assert.strictEqual(math.floor(1-1e-13), 1) + }) + }) From 814f66000024534fa6d8585007649d66485df44f Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Mon, 1 Aug 2022 16:24:20 -0700 Subject: [PATCH 13/22] feat(Chain): add computation pipelines like mathjs Also adds a `mean()` operation so there will be at least one operation that takes only a rest parameter, to exercise the ban on splitting such a parameter between the stored value and new arguments. Adds various tests of chains. Resolves #32. --- src/core/Chain.mjs | 70 +++++++++++++++++++++++++++++++++++ src/core/PocomathInstance.mjs | 8 ++++ src/generic/arithmetic.mjs | 1 + src/generic/mean.mjs | 3 ++ src/generic/multiply.mjs | 13 ------- test/_pocomath.mjs | 11 ++++++ test/custom.mjs | 8 +++- 7 files changed, 100 insertions(+), 14 deletions(-) create mode 100644 src/core/Chain.mjs create mode 100644 src/generic/mean.mjs delete mode 100644 src/generic/multiply.mjs diff --git a/src/core/Chain.mjs b/src/core/Chain.mjs new file mode 100644 index 0000000..11cb636 --- /dev/null +++ b/src/core/Chain.mjs @@ -0,0 +1,70 @@ +/* An object that holds a value and a reference to a PocomathInstance for + * applying operations to that value. Since the operations need to be wrapped, + * that instance is supposed to provide a place where wrapped operations can + * be stored, known as the repository. + */ +class Chain { + constructor(value, instance, repository) { + this.value = value + this.instance = instance + this.repository = repository + if (!('_wrapped' in this.repository)) { + this.repository._wrapped = {} + } + } + + /* This is the wrapper for func which calls it with the chain's + * current value inserted as the first argument. + */ + _chainify(func, typed) { + return function () { + // Here `this` is the proxied Chain instance + if (arguments.length === 0) { + this.value = func(this.value) + } else { + const args = [this.value, ...arguments] + if (typed.isTypedFunction(func)) { + const sigObject = typed.resolve(func, args) + if (sigObject.params.length === 1) { + throw new Error( + `chain function ${func.name} attempting to split a rest` + + 'parameter between chain value and other arguments') + } + this.value = sigObject.implementation.apply(func, args) + } else { + this.value = func.apply(func, args) + } + } + return this + } + } +} + +export function makeChain(value, instance, repository) { + const chainObj = new Chain(value, instance, repository) + /* Rather than using the chainObj directly, we Proxy it to + * ensure we only get back wrapped, current methods of the instance. + */ + return new Proxy(chainObj, { + get: (target, property) => { + if (property === 'value') return target.value + if (!(property in target.instance)) { + throw new SyntaxError(`Unknown operation ${property}`) + } + if (property.charAt(0) === '_') { + throw new SyntaxError(`No access to private ${property}`) + } + const curval = target.instance[property] + if (typeof curval !== 'function') { + throw new SyntaxError( + `Property ${property} does not designate an operation`) + } + if (curval != target.repository._wrapped[property]) { + target.repository._wrapped[property] = curval + target.repository[property] = target._chainify( + curval, target.instance._typed) + } + return target.repository[property] + } + }) +} diff --git a/src/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index 4858a54..c1cee4a 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -1,6 +1,7 @@ /* Core of pocomath: create an instance */ import typed from 'typed-function' import dependencyExtractor from './dependencyExtractor.mjs' +import {makeChain} from './Chain.mjs' import {subsetOfKeys, typesOfSignature} from './utils.mjs' const anySpec = {} // fixed dummy specification of 'any' type @@ -21,6 +22,7 @@ export default class PocomathInstance { * must be added to this list. */ static reserved = new Set([ + 'chain', 'config', 'importDependencies', 'install', @@ -64,6 +66,7 @@ export default class PocomathInstance { return true // successful } }) + this._chainRepository = {} // place to store chainified functions } /** @@ -164,6 +167,11 @@ export default class PocomathInstance { return result } + /* Return a chain object for this instance with a given value: */ + chain(value) { + return makeChain(value, this, this._chainRepository) + } + _installInstance(other) { for (const [type, spec] of Object.entries(other.Types)) { if (type === 'any' || this._templateParam(type)) continue diff --git a/src/generic/arithmetic.mjs b/src/generic/arithmetic.mjs index 7576dbe..f6abd23 100644 --- a/src/generic/arithmetic.mjs +++ b/src/generic/arithmetic.mjs @@ -4,6 +4,7 @@ export * from './Types/generic.mjs' export const add = reducingOperation export {lcm} from './lcm.mjs' +export {mean} from './mean.mjs' export {mod} from './mod.mjs' export const multiply = reducingOperation export {divide} from './divide.mjs' diff --git a/src/generic/mean.mjs b/src/generic/mean.mjs new file mode 100644 index 0000000..d12c21b --- /dev/null +++ b/src/generic/mean.mjs @@ -0,0 +1,3 @@ +export const mean = { + '...any': ({add, divide}) => args => divide(add(...args), args.length) +} diff --git a/src/generic/multiply.mjs b/src/generic/multiply.mjs deleted file mode 100644 index 63d196a..0000000 --- a/src/generic/multiply.mjs +++ /dev/null @@ -1,13 +0,0 @@ -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] - return later.reduce((x,y) => self(x,y), a) - } -} - diff --git a/test/_pocomath.mjs b/test/_pocomath.mjs index 3e6f2fe..222fbd6 100644 --- a/test/_pocomath.mjs +++ b/test/_pocomath.mjs @@ -82,4 +82,15 @@ describe('The default full pocomath instance "math"', () => { math.complex(11n, -4n)) assert.strictEqual(math.negate(math.complex(3n, 8n)).im, -8n) }) + + it('creates chains', () => { + const mychain = math.chain(7).negate() + assert.strictEqual(mychain.value, -7) + mychain.add(23).sqrt().lcm(10) + assert.strictEqual(mychain.value, 20) + assert.strictEqual(math.mean(3,4,5), 4) + assert.throws(() => math.chain(3).mean(4,5), /chain function.*split/) + assert.throws(() => math.chain(3).foo(), /Unknown operation/) + }) + }) diff --git a/test/custom.mjs b/test/custom.mjs index ba4c713..9fb66b1 100644 --- a/test/custom.mjs +++ b/test/custom.mjs @@ -17,7 +17,9 @@ describe('A custom instance', () => { it("works when partially assembled", () => { bw.install(complex) // Not much we can call without any number types: - assert.deepStrictEqual(bw.complex(0, 3), {re: 0, im: 3}) + const i3 = {re: 0, im: 3} + assert.deepStrictEqual(bw.complex(0, 3), i3) + assert.deepStrictEqual(bw.chain(0).complex(3).value, i3) // Don't have a way to negate things, for example: assert.throws(() => bw.negate(2), TypeError) }) @@ -40,6 +42,7 @@ describe('A custom instance', () => { assert.strictEqual(pm.subtract(5, 10), -5) assert.strictEqual(pm.floor(3.7), 3) assert.throws(() => pm.floor(10n), TypeError) + assert.strictEqual(pm.chain(5).add(7).value, 12) pm.install(complexAdd) pm.install(complexNegate) pm.install(complexComplex) @@ -52,6 +55,9 @@ describe('A custom instance', () => { assert.deepStrictEqual( pm.floor(math.complex(1.9, 0)), math.complex(1)) + // And the chain functions refresh themselves: + assert.deepStrictEqual( + pm.chain(5).add(pm.chain(0).complex(7).value).value, math.complex(5,7)) }) it("can defer definition of (even used) types", () => { From 8418817f9fabc3ef63d194f58c43b16f854ece4c Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Tue, 2 Aug 2022 00:47:37 -0700 Subject: [PATCH 14/22] feat(install): Allow plain functions --- src/bigint/all.mjs | 4 ++-- src/core/PocomathInstance.mjs | 22 +++++++++++++++++++++- src/number/all.mjs | 4 ++-- src/ops/all.mjs | 4 ++++ src/ops/choose.mjs | 11 +++++++++++ src/ops/factorial.mjs | 8 ++++++++ src/pocomath.mjs | 4 ++-- test/_pocomath.mjs | 10 ++++++++++ 8 files changed, 60 insertions(+), 7 deletions(-) create mode 100644 src/ops/all.mjs create mode 100644 src/ops/choose.mjs create mode 100644 src/ops/factorial.mjs diff --git a/src/bigint/all.mjs b/src/bigint/all.mjs index f9da9f0..6f1412d 100644 --- a/src/bigint/all.mjs +++ b/src/bigint/all.mjs @@ -1,6 +1,6 @@ import PocomathInstance from '../core/PocomathInstance.mjs' import * as bigints from './native.mjs' import * as generic from '../generic/all.mjs' -import * as floor from '../ops/floor.mjs' +import * as ops from '../ops/all.mjs' -export default PocomathInstance.merge('bigint', bigints, generic, floor) +export default PocomathInstance.merge('bigint', bigints, generic, ops) diff --git a/src/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index c1cee4a..6fab6c9 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -66,6 +66,7 @@ export default class PocomathInstance { return true // successful } }) + this._plainFunctions = new Set() // the names of the plain functions this._chainRepository = {} // place to store chainified functions } @@ -136,6 +137,8 @@ export default class PocomathInstance { for (const [item, spec] of Object.entries(ops)) { if (spec instanceof PocomathInstance) { this._installInstance(spec) + } else if (typeof spec === 'function') { + stdFunctions[item] = spec } else { if (item.charAt(0) === '_') { throw new SyntaxError( @@ -183,6 +186,9 @@ export default class PocomathInstance { migrateImps[operator] = other._imps[operator] } } + for (const plain of other._plainFunctions) { + migrateImps[plain] = other[plain] + } this._installFunctions(migrateImps) } @@ -352,7 +358,21 @@ export default class PocomathInstance { /* Used internally by install, see the documentation there */ _installFunctions(functions) { for (const [name, spec] of Object.entries(functions)) { - // new implementations, so set the op up to lazily recreate itself + if (typeof spec === 'function') { + if (name in this) { + if (spec === this[name]) continue + throw new SyntaxError(`Attempt to redefine function ${name}`) + } + this._plainFunctions.add(name) + this[name] = spec + continue + } + // new implementations, first check the name isn't taken + if (this._plainFunctions.has(name)) { + throw new SyntaxError( + `Can't add implementations to function ${name}`) + } + // All clear, so set the op up to lazily recreate itself this._invalidate(name) const opImps = this._imps[name] for (const [signature, behavior] of Object.entries(spec)) { diff --git a/src/number/all.mjs b/src/number/all.mjs index bedaa24..622484a 100644 --- a/src/number/all.mjs +++ b/src/number/all.mjs @@ -1,7 +1,7 @@ import PocomathInstance from '../core/PocomathInstance.mjs' import * as numbers from './native.mjs' import * as generic from '../generic/all.mjs' -import * as floor from '../ops/floor.mjs' +import * as ops from '../ops/all.mjs' -export default PocomathInstance.merge('number', numbers, generic, floor) +export default PocomathInstance.merge('number', numbers, generic, ops) diff --git a/src/ops/all.mjs b/src/ops/all.mjs new file mode 100644 index 0000000..3d1646b --- /dev/null +++ b/src/ops/all.mjs @@ -0,0 +1,4 @@ +export * from './choose.mjs' +export * from './factorial.mjs' +export * from './floor.mjs' + diff --git a/src/ops/choose.mjs b/src/ops/choose.mjs new file mode 100644 index 0000000..c285dc7 --- /dev/null +++ b/src/ops/choose.mjs @@ -0,0 +1,11 @@ +/* Note this is not a good algorithm for computing binomial coefficients, + * it's just for demonstration purposes + */ +export const choose = { + 'NumInt,NumInt': ({factorial}) => (n,k) => Number( + factorial(n) / (factorial(k)*factorial(n-k))), + 'bigint,bigint': ({ + factorial + }) => (n,k) => factorial(n) / (factorial(k)*factorial(n-k)) +} + diff --git a/src/ops/factorial.mjs b/src/ops/factorial.mjs new file mode 100644 index 0000000..bb07047 --- /dev/null +++ b/src/ops/factorial.mjs @@ -0,0 +1,8 @@ +export function factorial(n) { + n = BigInt(n) + let prod = 1n + for (let i = n; i > 1n; --i) { + prod *= i + } + return prod +} diff --git a/src/pocomath.mjs b/src/pocomath.mjs index cb9bf94..dee980e 100644 --- a/src/pocomath.mjs +++ b/src/pocomath.mjs @@ -4,9 +4,9 @@ import * as numbers from './number/native.mjs' import * as bigints from './bigint/native.mjs' import * as complex from './complex/native.mjs' import * as generic from './generic/all.mjs' -import * as floor from './ops/floor.mjs' +import * as ops from './ops/all.mjs' const math = PocomathInstance.merge( - 'math', numbers, bigints, complex, generic, floor) + 'math', numbers, bigints, complex, generic, ops) export default math diff --git a/test/_pocomath.mjs b/test/_pocomath.mjs index 222fbd6..18643b0 100644 --- a/test/_pocomath.mjs +++ b/test/_pocomath.mjs @@ -93,4 +93,14 @@ describe('The default full pocomath instance "math"', () => { assert.throws(() => math.chain(3).foo(), /Unknown operation/) }) + it('calls plain factorial function', () => { + assert.strictEqual(math.factorial(4), 24n) + assert.strictEqual(math.factorial(7n), 5040n) + }) + + it('calculates binomial coefficients', () => { + assert.strictEqual(math.choose(6, 3), 20) + assert.strictEqual(math.choose(21n, 2n), 210n) + }) + }) From 845a2354c988750df76f699f34d8ed75104b8c2b Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Fri, 5 Aug 2022 12:48:57 +0000 Subject: [PATCH 15/22] feat: Template types (#45) Includes a full implementation of a type-homogeneous Tuple type, using the template types feature, as a demonstration/check of its operation. Co-authored-by: Glen Whitney Reviewed-on: https://code.studioinfinity.org/glen/pocomath/pulls/45 --- src/bigint/native.mjs | 2 + src/complex/add.mjs | 8 +- src/complex/associate.mjs | 17 + src/complex/complex.mjs | 3 +- src/complex/{equal.mjs => equalTT.mjs} | 2 +- src/complex/gcd.mjs | 15 +- src/complex/native.mjs | 7 +- src/core/PocomathInstance.mjs | 613 +++++++++++++++--- src/core/dependencyExtractor.mjs | 12 - src/core/extractors.mjs | 41 ++ src/core/utils.mjs | 5 + src/generic/arithmetic.mjs | 2 + src/generic/identity.mjs | 3 + src/generic/lcm.mjs | 3 + src/generic/relational.mjs | 19 +- src/number/native.mjs | 2 + src/ops/floor.mjs | 2 +- src/pocomath.mjs | 3 +- src/tuple/Types/Tuple.mjs | 80 +++ src/tuple/equalTT.mjs | 11 + src/tuple/isZero.mjs | 8 + src/tuple/length.mjs | 3 + src/tuple/native.mjs | 21 + src/tuple/tuple.mjs | 6 + test/_pocomath.mjs | 9 + ...yExtractor.mjs => dependencyExtractor.mjs} | 2 +- test/custom.mjs | 36 +- test/tuple/_native.mjs | 114 ++++ 28 files changed, 920 insertions(+), 129 deletions(-) create mode 100644 src/complex/associate.mjs rename src/complex/{equal.mjs => equalTT.mjs} (95%) delete mode 100644 src/core/dependencyExtractor.mjs create mode 100644 src/core/extractors.mjs create mode 100644 src/generic/identity.mjs create mode 100644 src/tuple/Types/Tuple.mjs create mode 100644 src/tuple/equalTT.mjs create mode 100644 src/tuple/isZero.mjs create mode 100644 src/tuple/length.mjs create mode 100644 src/tuple/native.mjs create mode 100644 src/tuple/tuple.mjs rename test/core/{_dependencyExtractor.mjs => dependencyExtractor.mjs} (89%) create mode 100644 test/tuple/_native.mjs diff --git a/src/bigint/native.mjs b/src/bigint/native.mjs index dc12d9e..6458912 100644 --- a/src/bigint/native.mjs +++ b/src/bigint/native.mjs @@ -1,9 +1,11 @@ import gcdType from '../generic/gcdType.mjs' +import {identity} from '../generic/identity.mjs' export * from './Types/bigint.mjs' export {add} from './add.mjs' export {compare} from './compare.mjs' +export const conjugate = {bigint: () => identity} 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 1afd22f..0c178d6 100644 --- a/src/complex/add.mjs +++ b/src/complex/add.mjs @@ -7,16 +7,16 @@ export const add = { */ 'Complex,number': ({ 'self(number,number)': addNum, - 'complex(any,any)': cplx + 'complex(number,number)': cplx }) => (z,x) => cplx(addNum(z.re, x), z.im), 'Complex,bigint': ({ 'self(bigint,bigint)': addBigInt, - 'complex(any,any)': cplx + 'complex(bigint,bigint)': 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)) + complex + }) => (w,z) => complex(self(w.re, z.re), self(w.im, z.im)) } diff --git a/src/complex/associate.mjs b/src/complex/associate.mjs new file mode 100644 index 0000000..f3106b4 --- /dev/null +++ b/src/complex/associate.mjs @@ -0,0 +1,17 @@ +export * from './Types/Complex.mjs' + +/* Returns true if w is z multiplied by a complex unit */ +export const associate = { + 'Complex,Complex': ({ + 'multiply(Complex,Complex)': times, + 'equalTT(Complex,Complex)': eq, + zero, + one, + complex, + 'negate(Complex)': neg + }) => (w,z) => { + if (eq(w,z) || eq(w,neg(z))) return true + const ti = times(z, complex(zero(z.re), one(z.im))) + return eq(w,ti) || eq(w,neg(ti)) + } +} diff --git a/src/complex/complex.mjs b/src/complex/complex.mjs index 58d3e2e..c34434c 100644 --- a/src/complex/complex.mjs +++ b/src/complex/complex.mjs @@ -9,7 +9,8 @@ export const complex = { 'undefined': () => u => u, 'undefined,any': () => (u, y) => u, 'any,undefined': () => (x, u) => u, - 'any,any': () => (x, y) => ({re: x, im: y}), + 'undefined,undefined': () => (u, v) => u, + 'T,T': () => (x, y) => ({re: x, im: y}), /* Take advantage of conversions in typed-function */ Complex: () => z => z } diff --git a/src/complex/equal.mjs b/src/complex/equalTT.mjs similarity index 95% rename from src/complex/equal.mjs rename to src/complex/equalTT.mjs index 4eca63f..43fe0d1 100644 --- a/src/complex/equal.mjs +++ b/src/complex/equalTT.mjs @@ -1,6 +1,6 @@ export * from './Types/Complex.mjs' -export const equal = { +export const equalTT = { 'Complex,number': ({ 'isZero(number)': isZ, 'self(number,number)': eqNum diff --git a/src/complex/gcd.mjs b/src/complex/gcd.mjs index 84aa849..238d811 100644 --- a/src/complex/gcd.mjs +++ b/src/complex/gcd.mjs @@ -3,15 +3,18 @@ import * as Complex from './Types/Complex.mjs' import gcdType from '../generic/gcdType.mjs' const imps = { - gcdComplexRaw: gcdType('Complex'), + gcdGIRaw: gcdType('GaussianInteger'), gcd: { // Only return gcds with positive real part - 'Complex, Complex': ({gcdComplexRaw, sign, one, negate}) => (z,m) => { - const raw = gcdComplexRaw(z, m) - if (sign(raw.re) === one(raw.re)) return raw - return negate(raw) + 'GaussianInteger,GaussianInteger': ({ + 'gcdGIRaw(GaussianInteger,GaussianInteger)': gcdRaw, + 'sign(bigint)': sgn, + 'negate(GaussianInteger)': neg + }) => (z,m) => { + const raw = gcdRaw(z, m) + if (sgn(raw.re) === 1n) return raw + return neg(raw) } } } export const gcd = PocomathInstance.merge(Complex, imps) - diff --git a/src/complex/native.mjs b/src/complex/native.mjs index 70d6b14..420ce88 100644 --- a/src/complex/native.mjs +++ b/src/complex/native.mjs @@ -1,13 +1,12 @@ -import gcdType from '../generic/gcdType.mjs' - export * from './Types/Complex.mjs' export {abs} from './abs.mjs' export {absquare} from './absquare.mjs' export {add} from './add.mjs' -export {conjugate} from './conjugate.mjs' +export {associate} from './associate.mjs' export {complex} from './complex.mjs' -export {equal} from './equal.mjs' +export {conjugate} from './conjugate.mjs' +export {equalTT} from './equalTT.mjs' export {gcd} from './gcd.mjs' export {invert} from './invert.mjs' export {isZero} from './isZero.mjs' diff --git a/src/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index 6fab6c9..05636d4 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -1,12 +1,13 @@ /* Core of pocomath: create an instance */ import typed from 'typed-function' -import dependencyExtractor from './dependencyExtractor.mjs' +import {dependencyExtractor, generateTypeExtractor} from './extractors.mjs' import {makeChain} from './Chain.mjs' -import {subsetOfKeys, typesOfSignature} from './utils.mjs' +import {typeListOfSignature, typesOfSignature, subsetOfKeys} from './utils.mjs' const anySpec = {} // fixed dummy specification of 'any' type const theTemplateParam = 'T' // First pass: only allow this one exact parameter +const templateFromParam = 'U' // For defining covariant conversions /* Returns a new signature just like sig but with the parameter replaced by * the type @@ -27,7 +28,10 @@ export default class PocomathInstance { 'importDependencies', 'install', 'installType', + 'joinTypes', 'name', + 'self', + 'Templates', 'typeOf', 'Types', 'undefinedTypes' @@ -39,11 +43,17 @@ export default class PocomathInstance { this._affects = {} this._typed = typed.create() this._typed.clear() + this._typed.addTypes([{name: 'ground', test: () => true}]) /* 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.Types.ground = anySpec + // All the template types that have been defined + this.Templates = {} + // The actual type testing functions + this._typeTests = {} 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. @@ -52,8 +62,8 @@ export default class PocomathInstance { * 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._seenTypes = new Set() // all types that have occurred in a signature + this._invalid = new Set() // methods that are currently invalid this._config = {predictable: false, epsilon: 1e-12} const self = this this.config = new Proxy(this._config, { @@ -68,6 +78,12 @@ export default class PocomathInstance { }) this._plainFunctions = new Set() // the names of the plain functions this._chainRepository = {} // place to store chainified functions + + this._installFunctions({ + typeOf: {ground: {uses: new Set(), does: () => () => 'any'}} + }) + + this.joinTypes = this.joinTypes.bind(this) } /** @@ -177,9 +193,12 @@ export default class PocomathInstance { _installInstance(other) { for (const [type, spec] of Object.entries(other.Types)) { - if (type === 'any' || this._templateParam(type)) continue + if (spec === anySpec) continue this.installType(type, spec) } + for (const [base, info] of Object.entries(other.Templates)) { + this._installTemplateType(info.type, info.spec) + } const migrateImps = {} for (const operator in other._imps) { if (operator != 'typeOf') { // skip the builtin, we already have it @@ -262,10 +281,15 @@ export default class PocomathInstance { * the corresponding changes to the _typed object immediately */ installType(type, spec) { - if (this._templateParam(type)) { + const parts = type.split(/[<,>]/) + if (this._templateParam(parts[0])) { throw new SyntaxError( `Type name '${type}' reserved for template parameter`) } + if (parts.some(this._templateParam.bind(this))) { + // It's a template, deal with it separately + return this._installTemplateType(type, spec) + } if (type in this.Types) { if (spec !== this.Types[type]) { throw new SyntaxError(`Conflicting definitions of type ${type}`) @@ -278,7 +302,7 @@ export default class PocomathInstance { } let beforeType = spec.refines if (!beforeType) { - beforeType = 'any' + beforeType = 'ground' for (const other of spec.before || []) { if (other in this.Types) { beforeType = other @@ -291,68 +315,129 @@ export default class PocomathInstance { const supertypeTest = this.Types[spec.refines].test testFn = entity => supertypeTest(entity) && spec.test(entity) } + this._typeTests[type] = testFn this._typed.addTypes([{name: type, test: testFn}], beforeType) this.Types[type] = spec + this._subtypes[type] = new Set() this._priorTypes[type] = new Set() + // Update all the subtype sets of supertypes up the chain + let nextSuper = spec.refines + while (nextSuper) { + this._invalidateDependents(':' + nextSuper) + this._priorTypes[nextSuper].add(type) + this._subtypes[nextSuper].add(type) + nextSuper = this.Types[nextSuper].refines + } /* Now add conversions to this type */ for (const from in (spec.from || {})) { if (from in this.Types) { // add conversions from "from" to this one and all its supertypes: let nextSuper = type while (nextSuper) { + if (this._priorTypes[nextSuper].has(from)) break this._typed.addConversion( {from, to: nextSuper, convert: spec.from[from]}) this._invalidateDependents(':' + nextSuper) this._priorTypes[nextSuper].add(from) + /* And all of the subtypes of from are now prior as well: */ + for (const subtype of this._subtypes[from]) { + this._priorTypes[nextSuper].add(subtype) + } nextSuper = this.Types[nextSuper].refines } } } /* And add conversions from this type */ for (const to in this.Types) { - if (type in (this.Types[to].from || {})) { - if (spec.refines == to || spec.refines in this._subtypes[to]) { - throw new SyntaxError( - `Conversion of ${type} to its supertype ${to} disallowed.`) - } - let nextSuper = to - while (nextSuper) { - this._typed.addConversion({ - from: type, - to: nextSuper, - convert: this.Types[to].from[type] - }) - this._invalidateDependents(':' + nextSuper) - this._priorTypes[nextSuper].add(type) - nextSuper = this.Types[nextSuper].refines + for (const fromtype in this.Types[to].from) { + if (type == fromtype + || (fromtype in this._subtypes + && this._subtypes[fromtype].has(type))) { + if (spec.refines == to || spec.refines in this._subtypes[to]) { + throw new SyntaxError( + `Conversion of ${type} to its supertype ${to} disallowed.`) + } + let nextSuper = to + while (nextSuper) { + this._typed.addConversion({ + from: type, + to: nextSuper, + convert: this.Types[to].from[fromtype] + }) + this._invalidateDependents(':' + nextSuper) + this._priorTypes[nextSuper].add(type) + nextSuper = this.Types[nextSuper].refines + } } } } - // 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() - 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 - } - // update the typeOf function const imp = {} imp[type] = {uses: new Set(), does: () => () => type} this._installFunctions({typeOf: imp}) } + /* Returns the most refined type of all the types in the array, with + * '' standing for the empty type for convenience. If the second + * argument `convert` is true, a convertible type is considered a + * a subtype (defaults to false). + */ + joinTypes(types, convert) { + let join = '' + for (const type of types) { + join = this._joinTypes(join, type, convert) + } + return join + } + /* helper for above */ + _joinTypes(typeA, typeB, convert) { + if (!typeA) return typeB + if (!typeB) return typeA + if (typeA === 'any' || typeB === 'any') return 'any' + if (typeA === 'ground' || typeB === 'ground') return 'ground' + if (typeA === typeB) return typeA + const subber = convert ? this._priorTypes : this._subtypes + if (subber[typeB].has(typeA)) return typeB + /* OK, so we need the most refined supertype of A that contains B: + */ + let nextSuper = typeA + while (nextSuper) { + if (subber[nextSuper].has(typeB)) return nextSuper + nextSuper = this.Types[nextSuper].refines + } + /* And if conversions are allowed, we have to search the other way too */ + if (convert) { + nextSuper = typeB + while (nextSuper) { + if (subber[nextSuper].has(typeA)) return nextSuper + nextSuper = this.Types[nextSuper].refines + } + } + return 'any' + } + /* Returns a list of all types that have been mentioned in the * signatures of operations, but which have not actually been installed: */ undefinedTypes() { - return Array.from(this._usedTypes).filter(t => !(t in this.Types)) + return Array.from(this._seenTypes).filter(t => !(t in this.Types)) + } + + /* Used internally to install a template type */ + _installTemplateType(type, spec) { + const base = type.split('<')[0] + /* For now, just allow a single template per base type; that + * might need to change later: + */ + if (base in this.Templates) { + if (spec !== this.Templates[base].spec) { + throw new SyntaxError( + `Conflicting definitions of template type ${type}`) + } + return + } + // Nothing actually happens until we match a template parameter + this.Templates[base] = {type, spec} } /* Used internally by install, see the documentation there */ @@ -392,9 +477,12 @@ export default class PocomathInstance { this._addAffect(depname, name) } for (const type of typesOfSignature(signature)) { - if (this._templateParam(type)) continue - this._usedTypes.add(type) - this._addAffect(':' + type, name) + for (const word of type.split(/[<>]/)) { + if (word.length == 0) continue + if (this._templateParam(word)) continue + this._seenTypes.add(word) + this._addAffect(':' + word, name) + } } } } @@ -420,20 +508,20 @@ export default class PocomathInstance { * and if it has no implementations so far, set them up. */ _invalidate(name) { - if (this._doomed.has(name)) { - /* In the midst of a circular invalidation, so do nothing */ - return - } + if (this._invalid.has(name)) return if (!(name in this._imps)) { this._imps[name] = {} } - this._doomed.add(name) + this._invalid.add(name) this._invalidateDependents(name) - this._doomed.delete(name) const self = this Object.defineProperty(this, name, { configurable: true, - get: () => self._bundle(name) + get: () => { + const result = self._bundle(name) + self._invalid.delete(name) + return result + } }) } @@ -457,22 +545,38 @@ export default class PocomathInstance { if (!imps) { throw new SyntaxError(`No implementations for ${name}`) } - const usableEntries = Object.entries(imps).filter( - ([signature]) => subsetOfKeys(typesOfSignature(signature), this.Types)) + /* Collect the entries we know the types for */ + const usableEntries = [] + for (const entry of Object.entries(imps)) { + let keep = true + for (const type of typesOfSignature(entry[0])) { + if (type in this.Types) continue + const baseType = type.split('<')[0] + if (baseType in this.Templates) continue + keep = false + break + } + if (keep) usableEntries.push(entry) + } if (usableEntries.length === 0) { throw new SyntaxError( `Every implementation for ${name} uses an undefined type;\n` + ` signatures: ${Object.keys(imps)}`) } + /* Initial error checking done; mark this method as being + * in the midst of being reassembled + */ Object.defineProperty(this, name, {configurable: true, value: 'limbo'}) const tf_imps = {} 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 + for (const word of type.split(/[<>]/)) { + if (this._templateParam(word)) { + explicit = false + break + } } } if (explicit) { @@ -485,16 +589,10 @@ export default class PocomathInstance { 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 behavior.instantiations) { + instantiationSet.add(instType) + for (const other of this._priorTypes[instType]) { + instantiationSet.add(other) } } @@ -502,7 +600,7 @@ export default class PocomathInstance { if (!(instType in this.Types)) continue if (this.Types[instType] === anySpec) continue const signature = - substituteInSig(trimSignature, theTemplateParam, instType) + substituteInSig(rawSignature, theTemplateParam, instType) /* Don't override an explicit implementation: */ if (signature in imps) continue const uses = new Set() @@ -521,44 +619,170 @@ export default class PocomathInstance { innerRefs[dep] = refs[outerName] } } - const original = behavior.does(innerRefs) return behavior.does(innerRefs) } - this._addTFimplementation(tf_imps, signature, {uses, does: patch}) + this._addTFimplementation( + tf_imps, signature, {uses, does: patch}) } /* Now add the catchall signature */ + let templateCall = `<${theTemplateParam}>` + /* Relying here that the base of 'Foo' is 'Foo': */ + let baseSignature = rawSignature.replaceAll(templateCall, '') + /* Any remaining template params are top-level */ const signature = substituteInSig( - trimSignature, theTemplateParam, 'any') + baseSignature, theTemplateParam, 'ground') /* The catchall signature has to detect the actual type of the call - * and add the new instantiations + * and add the new instantiations. + * First, prepare the type inference data: */ - 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) { + const parTypes = rawSignature.split(',') + const restParam = (parTypes[parTypes.length-1].slice(0,3) === '...') + const topTyper = entity => this.typeOf(entity) + const inferences = parTypes.map( + type => generateTypeExtractor( + type, + theTemplateParam, + topTyper, + this.joinTypes.bind(this), + this.Templates)) + if (inferences.every(x => !x)) { // all false throw new SyntaxError( `Cannot find template parameter in ${rawSignature}`) } + /* And eliminate template parameters from the dependencies */ + const simplifiedUses = {} + for (const dep of behavior.uses) { + let [func, needsig] = dep.split(/[()]/) + if (needsig) { + const subsig = substituteInSig(needsig, theTemplateParam, '') + if (subsig === needsig) { + simplifiedUses[dep] = dep + } else { + simplifiedUses[dep] = func + } + } else { + simplifiedUses[dep] = dep + } + } + /* Now build the catchall implementation */ const self = this const patch = (refs) => (...args) => { - const example = args[exemplar] - const instantiateFor = self.typeOf(example) + /* We unbundle the rest arg if there is one */ + const regLength = args.length - 1 + if (restParam) { + const restArgs = args.pop() + args = args.concat(restArgs) + } + /* Now infer the type we actually should have been called for */ + let i = -1 + let j = -1 + /* collect the arg types */ + const argTypes = [] + for (const arg of args) { + ++j + // in case of rest parameter, reuse last parameter type: + if (i < inferences.length - 1) ++i + if (inferences[i]) { + const argType = inferences[i](arg) + if (!argType) { + throw TypeError( + `Type inference failed for argument ${j} of ${name}`) + } + if (argType === 'any') { + throw TypeError( + `In call to ${name}, incompatible template arguments: ` + + args.map(a => JSON.stringify(a)).join(', ')) + } + argTypes.push(argType) + } + } + if (argTypes.length === 0) { + throw TypeError('Type inference failed for' + name) + } + let usedConversions = false + let instantiateFor = self.joinTypes(argTypes) + if (instantiateFor === 'any') { + usedConversions = true + instantiateFor = self.joinTypes(argTypes, usedConversions) + if (instantiateFor === 'any') { + throw TypeError( + `In call to ${name}, no type unifies arguments ` + + args.toString() + '; of types ' + argTypes.toString() + + '; note each consecutive pair must unify to a ' + + 'supertype of at least one of them') + } + } + /* Generate the list of actual wanted types */ + const wantTypes = parTypes.map(type => substituteInSig( + type, theTemplateParam, instantiateFor)) + /* Now we have to add any actual types that are relevant + * to this invocation. Namely, that would be every formal parameter + * type in the invocation, with the parameter template instantiated + * by instantiateFor, and for all of instantiateFor's "prior types" + */ + for (j = 0; j < parTypes.length; ++j) { + if (wantTypes[j] !== parTypes[j] && parTypes[j].includes('<')) { + // actually used the param and is a template + self._ensureTemplateTypes(parTypes[j], instantiateFor) + } + } + /* Transform the arguments if we used any conversions: */ + if (usedConversions) { + i = - 1 + for (j = 0; j < args.length; ++j) { + if (i < parTypes.length - 1) ++i + let wantType = parTypes[i] + if (wantType.slice(0,3) === '...') { + wantType = wantType.slice(3) + } + wantType = substituteInSig( + wantType, theTemplateParam, instantiateFor) + if (wantType !== parTypes[i]) { + args[j] = self._typed.convert(args[j], wantType) + } + } + } + /* Finally reassemble the rest args if there were any */ + if (restParam) { + const restArgs = args.slice(regLength) + args = args.slice(0,regLength) + args.push(restArgs) + } + /* Arrange that the desired instantiation will be there next + * time so we don't have to go through that again for this type + */ 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) + // And update refs because we now know the type we're instantiating + // for: + const innerRefs = {} + for (const dep in simplifiedUses) { + const simplifiedDep = simplifiedUses[dep] + if (dep === simplifiedDep) { + innerRefs[dep] = refs[dep] + } else { + let [func, needsig] = dep.split(/[()]/) + if (self._typed.isTypedFunction(refs[simplifiedDep])) { + const subsig = substituteInSig( + needsig, theTemplateParam, instantiateFor) + let resname = simplifiedDep + if (resname === 'self') resname = name + innerRefs[dep] = self._pocoresolve(resname, subsig) + } else { + innerRefs[dep] = refs[simplifiedDep] + } + } + } + // Finally ready to make the call. + return behavior.does(innerRefs)(...args) } + // The actual uses value needs to be a set: + const outerUses = new Set(Object.values(simplifiedUses)) this._addTFimplementation( - tf_imps, signature, {uses: behavior.uses, does: patch}) + tf_imps, signature, {uses: outerUses, does: patch}) } + this._correctPartialSelfRefs(tf_imps) const tf = this._typed(name, tf_imps) Object.defineProperty(this, name, {configurable: true, value: tf}) return tf @@ -579,9 +803,17 @@ export default class PocomathInstance { 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 = '' + /* Safety check that can perhaps be removed: + * Verify that the desired signature has been fully grounded: + */ + if (needsig) { + const trysig = substituteInSig(needsig, theTemplateParam, '') + if (trysig !== needsig) { + throw new Error( + 'Attempt to add a template implementation: ' + + `${signature} with dependency ${dep}`) + } + } if (func === 'self') { if (needsig) { if (full_self_referential) { @@ -614,7 +846,7 @@ export default class PocomathInstance { // can bundle up func, and grab its signature if need be let destination = this[func] if (needsig) { - destination = this._typed.find(destination, needsig) + destination = this._pocoresolve(func, needsig) } refs[dep] = destination } @@ -628,16 +860,215 @@ export default class PocomathInstance { return } if (part_self_references.length) { - imps[signature] = this._typed.referTo( - ...part_self_references, (...impls) => { + /* There is an obstruction here. The list part_self_references + * might contain a signature that requires conversion for self to + * handle. But I advocated this not be allowed in typed.referTo, which + * made sense for human-written functions, but is unfortunate now. + * So we have to defer creating these and correct them later, at + * least until we can add an option to typed-function. + */ + imps[signature] = { + deferred: true, + builtRefs: refs, + sigDoes: does, + psr: part_self_references + } + return + } + imps[signature] = does(refs) + } + + _correctPartialSelfRefs(imps) { + for (const aSignature in imps) { + if (!(imps[aSignature].deferred)) continue + const part_self_references = imps[aSignature].psr + const corrected_self_references = [] + for (const neededSig of part_self_references) { + // Have to find a match for neededSig among the other signatures + // of this function. That's a job for typed-function, but we will + // try here: + if (neededSig in imps) { // the easy case + corrected_self_references.push(neededSig) + continue + } + // No exact match, try to get one that matches with + // subtypes since the whole conversion thing in typed-function + // is too complicated to reproduce + const foundSig = this._findSubtypeImpl(imps, neededSig) + if (foundSig) { + corrected_self_references.push(foundSig) + } else { + throw new Error( + 'Implement inexact self-reference in typed-function for ' + + neededSig) + } + } + const refs = imps[aSignature].builtRefs + const does = imps[aSignature].sigDoes + imps[aSignature] = this._typed.referTo( + ...corrected_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) } + + /* This function analyzes the template and makes sure the + * instantiations of it for type and all prior types of type are present + * in the instance. + */ + _ensureTemplateTypes(template, type) { + let [base, arg] = template.split('<', 2) + arg = arg.slice(0,-1) + if (!arg) { + throw new Error( + 'Implementation error in _ensureTemplateTypes', template, type) + } + let instantiations + if (this._templateParam(arg)) { // 1st-level template + instantiations = new Set(this._priorTypes[type]) + instantiations.add(type) + } else { // nested template + instantiations = this._ensureTemplateTypes(arg, type) + } + const resultingTypes = new Set() + for (const iType of instantiations) { + const resultType = this._maybeAddTemplateType(base, iType) + if (resultType) resultingTypes.add(resultType) + } + return resultingTypes + } + + /* Maybe add the instantiation of template type base with argument tyoe + * instantiator to the Types of this instance, if it hasn't happened already. + * Returns the name of the type if added, false otherwise. + */ + _maybeAddTemplateType(base, instantiator) { + const wantsType = `${base}<${instantiator}>` + if (wantsType in this.Types) return false + // OK, need to generate the type from the template + // Set up refines, before, test, and from + const newTypeSpec = {refines: base} + const maybeFrom = {} + const template = this.Templates[base].spec + if (!template) { + throw new Error( + `Implementor error in _maybeAddTemplateType ${base} ${instantiator}`) + } + const instantiatorSpec = this.Types[instantiator] + let beforeTypes = [] + if (instantiatorSpec.before) { + beforeTypes = instantiatorSpec.before.map(type => `${base}<${type}>`) + } + if (template.before) { + for (const beforeTmpl of template.before) { + beforeTypes.push( + substituteInSig(beforeTmpl, theTemplateParam, instantiator)) + } + } + if (beforeTypes.length > 0) { + newTypeSpec.before = beforeTypes + } + newTypeSpec.test = template.test(this._typeTests[instantiator]) + if (template.from) { + for (let source in template.from) { + const instSource = substituteInSig( + source, theTemplateParam, instantiator) + let usesFromParam = false + for (const word of instSource.split(/[<>]/)) { + if (word === templateFromParam) { + usesFromParam = true + break + } + } + if (usesFromParam) { + for (const iFrom in instantiatorSpec.from) { + const finalSource = substituteInSig( + instSource, templateFromParam, iFrom) + maybeFrom[finalSource] = template.from[source]( + instantiatorSpec.from[iFrom]) + } + // Assuming all templates are covariant here, I guess... + for (const subType of this._subtypes[instantiator]) { + const finalSource = substituteInSig( + instSource, templateFromParam, subType) + maybeFrom[finalSource] = template.from[source](x => x) + } + } else { + maybeFrom[instSource] = template.from[source] + } + } + } + + if (Object.keys(maybeFrom).length > 0) { + newTypeSpec.from = maybeFrom + } + this.installType(wantsType, newTypeSpec) + return wantsType + } + + _findSubtypeImpl(imps, neededSig) { + if (neededSig in imps) return neededSig + let foundSig = false + const typeList = typeListOfSignature(neededSig) + for (const otherSig in imps) { + const otherTypeList = typeListOfSignature(otherSig) + if (typeList.length !== otherTypeList.length) continue + let allMatch = true + for (let k = 0; k < typeList.length; ++k) { + let myType = typeList[k] + let otherType = otherTypeList[k] + if (otherType === theTemplateParam) { + otherTypeList[k] = 'ground' + otherType = 'ground' + } + if (otherType === '...T') { + otherTypeList[k] = '...ground' + otherType = 'ground' + } + const adjustedOtherType = otherType.replaceAll( + `<${theTemplateParam}>`, '') + if (adjustedOtherType !== otherType) { + otherTypeList[k] = adjustedOtherType + otherType = adjustedOtherType + } + if (myType.slice(0,3) === '...') myType = myType.slice(3) + if (otherType.slice(0,3) === '...') otherType = otherType.slice(3) + if (otherType === 'any') continue + if (otherType === 'ground') continue + if (!(otherType in this.Types)) { + allMatch = false + break + } + if (myType === otherType + || this._subtypes[otherType].has(myType)) { + continue + } + allMatch = false + break + } + if (allMatch) { + foundSig = otherTypeList.join(',') + break + } + } + return foundSig + } + + _pocoresolve(name, sig) { + const typedfunc = this[name] + let result = undefined + try { + result = this._typed.find(typedfunc, sig, {exact: true}) + } catch { + } + if (result) return result + const foundsig = this._findSubtypeImpl(this._imps[name], sig) + if (foundsig) return this._typed.find(typedfunc, foundsig) + return this._typed.find(typedfunc, sig) + } + } diff --git a/src/core/dependencyExtractor.mjs b/src/core/dependencyExtractor.mjs deleted file mode 100644 index 1b1091c..0000000 --- a/src/core/dependencyExtractor.mjs +++ /dev/null @@ -1,12 +0,0 @@ -/* Call this with an empty Set object S, and it returns an entity E - * from which properties can be extracted, and at any time S will - * contain all of the property names that have been extracted from E. - */ -export default function dependencyExtractor(destinationSet) { - return new Proxy({}, { - get: (target, property) => { - destinationSet.add(property) - return {} - } - }) -} diff --git a/src/core/extractors.mjs b/src/core/extractors.mjs new file mode 100644 index 0000000..0db3c0f --- /dev/null +++ b/src/core/extractors.mjs @@ -0,0 +1,41 @@ +/* Call this with an empty Set object S, and it returns an entity E + * from which properties can be extracted, and at any time S will + * contain all of the property names that have been extracted from E. + */ +export function dependencyExtractor(destinationSet) { + return new Proxy({}, { + get: (target, property) => { + destinationSet.add(property) + return {} + } + }) +} + +/* Given a (template) type name, what the template parameter is, + * a top level typer, and a library of templates, + * produces a function that will extract the instantantion type from an + * instance. Currently relies heavily on there being only unary templates. + * + * We should really be using the typed-function parser to do the + * manipulations below, but at the moment we don't have access. + */ +export function generateTypeExtractor( + type, param, topTyper, typeJoiner, templates) +{ + type = type.trim() + if (type.slice(0,3) === '...') { + type = type.slice(3).trim() + } + if (type === param) return topTyper + if (!(type.includes('<'))) return false // no template type to extract + const base = type.split('<',1)[0] + if (!(base in templates)) return false // unknown template + const arg = type.slice(base.length+1, -1) + const argExtractor = generateTypeExtractor( + arg, param, topTyper, typeJoiner, templates) + if (!argExtractor) return false + return templates[base].spec.infer({ + typeOf: argExtractor, + joinTypes: typeJoiner + }) +} diff --git a/src/core/utils.mjs b/src/core/utils.mjs index 11a879f..db164dd 100644 --- a/src/core/utils.mjs +++ b/src/core/utils.mjs @@ -6,6 +6,11 @@ export function subsetOfKeys(set, obj) { return true } +/* Returns a list of the types mentioned in a typed-function signature */ +export function typeListOfSignature(signature) { + return signature.split(',').map(s => s.trim()) +} + /* Returns a set of all of the types mentioned in a typed-function signature */ export function typesOfSignature(signature) { return new Set(signature.split(/[^\w\d]/).filter(s => s.length)) diff --git a/src/generic/arithmetic.mjs b/src/generic/arithmetic.mjs index f6abd23..00faddb 100644 --- a/src/generic/arithmetic.mjs +++ b/src/generic/arithmetic.mjs @@ -3,6 +3,8 @@ import {reducingOperation} from './reducingOperation.mjs' export * from './Types/generic.mjs' export const add = reducingOperation +export const gcd = reducingOperation +export {identity} from './identity.mjs' export {lcm} from './lcm.mjs' export {mean} from './mean.mjs' export {mod} from './mod.mjs' diff --git a/src/generic/identity.mjs b/src/generic/identity.mjs new file mode 100644 index 0000000..2422d2f --- /dev/null +++ b/src/generic/identity.mjs @@ -0,0 +1,3 @@ +export function identity(x) { + return x +} diff --git a/src/generic/lcm.mjs b/src/generic/lcm.mjs index adc3dfb..04e78b5 100644 --- a/src/generic/lcm.mjs +++ b/src/generic/lcm.mjs @@ -1,3 +1,5 @@ +import {reducingOperation} from './reducingOperation.mjs' + export const lcm = { 'T,T': ({ 'multiply(T,T)': multT, @@ -5,3 +7,4 @@ export const lcm = { 'gcd(T,T)': gcdT }) => (a,b) => multT(quotT(a, gcdT(a,b)), b) } +Object.assign(lcm, reducingOperation) diff --git a/src/generic/relational.mjs b/src/generic/relational.mjs index 368f394..939ae19 100644 --- a/src/generic/relational.mjs +++ b/src/generic/relational.mjs @@ -7,14 +7,27 @@ export const isZero = { } export const equal = { - '!T,T': ({ + 'any,any': ({equalTT, joinTypes, Templates, typeOf}) => (x,y) => { + const resultant = joinTypes([typeOf(x), typeOf(y)], 'convert') + if (resultant === 'any' || resultant in Templates) { + return false + } + return equalTT(x,y) + } +} + +export const equalTT = { + 'T,T': ({ 'compare(T,T)': cmp, 'isZero(T)': isZ - }) => (x,y) => isZ(cmp(x,y)) + }) => (x,y) => isZ(cmp(x,y)), + // If templates were native to typed-function, we should be able to + // do something like: + // 'any,any': () => () => false // should only be hit for different types } export const unequal = { - 'T,T': ({'equal(T.T)': eq}) => (x,y) => !(eq(x,y)) + 'any,any': ({equal}) => (x,y) => !(equal(x,y)) } export const larger = { diff --git a/src/number/native.mjs b/src/number/native.mjs index 1404ab4..d095574 100644 --- a/src/number/native.mjs +++ b/src/number/native.mjs @@ -1,10 +1,12 @@ import gcdType from '../generic/gcdType.mjs' +import {identity} from '../generic/identity.mjs' export * from './Types/number.mjs' export {abs} from './abs.mjs' export {add} from './add.mjs' export {compare} from './compare.mjs' +export const conjugate = {number: () => identity} export const gcd = gcdType('NumInt') export {invert} from './invert.mjs' export {isZero} from './isZero.mjs' diff --git a/src/ops/floor.mjs b/src/ops/floor.mjs index 98d524a..e8897e8 100644 --- a/src/ops/floor.mjs +++ b/src/ops/floor.mjs @@ -12,7 +12,7 @@ export const floor = { // entry with type `bigint|NumInt|GaussianInteger` because they couldn't // be separately activated then - number: ({'equal(number,number)': eq}) => n => { + number: ({'equalTT(number,number)': eq}) => n => { if (eq(n, Math.round(n))) return Math.round(n) return Math.floor(n) }, diff --git a/src/pocomath.mjs b/src/pocomath.mjs index dee980e..d8df045 100644 --- a/src/pocomath.mjs +++ b/src/pocomath.mjs @@ -3,10 +3,11 @@ import PocomathInstance from './core/PocomathInstance.mjs' import * as numbers from './number/native.mjs' import * as bigints from './bigint/native.mjs' import * as complex from './complex/native.mjs' +import * as tuple from './tuple/native.mjs' import * as generic from './generic/all.mjs' import * as ops from './ops/all.mjs' const math = PocomathInstance.merge( - 'math', numbers, bigints, complex, generic, ops) + 'math', numbers, bigints, complex, tuple, generic, ops) export default math diff --git a/src/tuple/Types/Tuple.mjs b/src/tuple/Types/Tuple.mjs new file mode 100644 index 0000000..0c6c0ae --- /dev/null +++ b/src/tuple/Types/Tuple.mjs @@ -0,0 +1,80 @@ +/* A template type representing a homeogeneous tuple of elements */ +import PocomathInstance from '../../core/PocomathInstance.mjs' + +const Tuple = new PocomathInstance('Tuple') +// First a base type that will generally not be used directly +Tuple.installType('Tuple', { + test: t => t && typeof t === 'object' && 'elts' in t && Array.isArray(t.elts) +}) +// Now the template type that is the primary use of this +Tuple.installType('Tuple', { + // We are assuming that any 'Type' refines 'Type', so this is + // not necessary: + // refines: 'Tuple', + // But we need there to be a way to determine the type of a tuple: + infer: ({typeOf, joinTypes}) => t => joinTypes(t.elts.map(typeOf)), + // For the test, we can assume that t is already a base tuple, + // and we get the test for T as an input and we have to return + // the test for Tuple + test: testT => t => t.elts.every(testT), + // These are only invoked for types U such that there is already + // a conversion from U to T, and that conversion is passed as an input + // and we have to return the conversion to Tuple: + from: { + 'Tuple': convert => tu => ({elts: tu.elts.map(convert)}), + // Here since there is no U it's a straight conversion: + T: t => ({elts: [t]}), // singleton promotion + // Whereas the following will let you go directly from an element + // convertible to T to a singleton Tuple. Not sure if we really + // want that, but we'll try it just for kicks. + U: convert => u => ({elts: [convert(u)]}) + } +}) + +Tuple.promoteUnary = { + 'Tuple': ({'self(T)': me, tuple}) => t => tuple(...(t.elts.map(me))) +} + +Tuple.promoteBinaryUnary = { + 'Tuple,Tuple': ({'self(T,T)': meB, 'self(T)': meU, tuple}) => (s,t) => { + let i = -1 + let result = [] + while (true) { + i += 1 + if (i < s.elts.length) { + if (i < t.elts.length) result.push(meB(s.elts[i], t.elts[i])) + else result.push(meU(s.elts[i])) + continue + } + if (i < t.elts.length) result.push(meU(t.elts[i])) + else break + } + return tuple(...result) + } +} + +Tuple.promoteBinary = { + 'Tuple,Tuple': ({'self(T,T)': meB, tuple}) => (s,t) => { + const lim = Math.max(s.elts.length, t.elts.length) + const result = [] + for (let i = 0; i < lim; ++i) { + result.push(meB(s.elts[i], t.elts[i])) + } + return tuple(...result) + } +} + +Tuple.promoteBinaryStrict = { + 'Tuple,Tuple': ({'self(T,T)': meB, tuple}) => (s,t) => { + if (s.elts.length !== t.elts.length) { + throw new RangeError('Tuple length mismatch') // get name of self ?? + } + const result = [] + for (let i = 0; i < s.elts.length; ++i) { + result.push(meB(s.elts[i], t.elts[i])) + } + return tuple(...result) + } +} + +export {Tuple} diff --git a/src/tuple/equalTT.mjs b/src/tuple/equalTT.mjs new file mode 100644 index 0000000..1606410 --- /dev/null +++ b/src/tuple/equalTT.mjs @@ -0,0 +1,11 @@ +export * from './Types/Tuple.mjs' + +export const equalTT = { + 'Tuple,Tuple': ({'self(T,T)': me, 'length(Tuple)': len}) => (s,t) => { + if (len(s) !== len(t)) return false + for (let i = 0; i < len(s); ++i) { + if (!me(s.elts[i], t.elts[i])) return false + } + return true + } +} diff --git a/src/tuple/isZero.mjs b/src/tuple/isZero.mjs new file mode 100644 index 0000000..9375277 --- /dev/null +++ b/src/tuple/isZero.mjs @@ -0,0 +1,8 @@ +export {Tuple} from './Types/Tuple.mjs' + +export const isZero = { + 'Tuple': ({'self(T)': me}) => t => t.elts.every(e => me(e)) + // Note we can't just say `every(me)` above since every invokes its + // callback with more arguments, which then violates typed-function's + // signature for `me` +} diff --git a/src/tuple/length.mjs b/src/tuple/length.mjs new file mode 100644 index 0000000..f3e8f2d --- /dev/null +++ b/src/tuple/length.mjs @@ -0,0 +1,3 @@ +export {Tuple} from './Types/Tuple.mjs' + +export const length = {Tuple: () => t => t.elts.length} diff --git a/src/tuple/native.mjs b/src/tuple/native.mjs new file mode 100644 index 0000000..66ba8f0 --- /dev/null +++ b/src/tuple/native.mjs @@ -0,0 +1,21 @@ +import {Tuple} from './Types/Tuple.mjs' + +export const add = Tuple.promoteBinaryUnary +export const complex = Tuple.promoteBinaryStrict +export const conjugate = Tuple.promoteUnary +export const divide = Tuple.promoteBinaryStrict +export {equalTT} from './equalTT.mjs' +export const invert = Tuple.promoteUnary +export {isZero} from './isZero.mjs' +export {length} from './length.mjs' +export const multiply = Tuple.promoteBinaryUnary +export const negate = Tuple.promoteUnary +export const one = Tuple.promoteUnary +export const quotient = Tuple.promoteBinaryStrict +export const roundquotient = Tuple.promoteBinaryStrict +export const sqrt = Tuple.promoteUnary +export const subtract = Tuple.promoteBinaryStrict +export {tuple} from './tuple.mjs' +export const zero = Tuple.promoteUnary + +export {Tuple} diff --git a/src/tuple/tuple.mjs b/src/tuple/tuple.mjs new file mode 100644 index 0000000..893b54d --- /dev/null +++ b/src/tuple/tuple.mjs @@ -0,0 +1,6 @@ +export {Tuple} from './Types/Tuple.mjs' + +/* The purpose of the template argument is to ensure that all of the args + * are convertible to the same type. + */ +export const tuple = {'...T': () => args => ({elts: args})} diff --git a/test/_pocomath.mjs b/test/_pocomath.mjs index 18643b0..1d7d1e9 100644 --- a/test/_pocomath.mjs +++ b/test/_pocomath.mjs @@ -103,4 +103,13 @@ describe('The default full pocomath instance "math"', () => { assert.strictEqual(math.choose(21n, 2n), 210n) }) + it('calculates multi-way gcds and lcms', () => { + assert.strictEqual(math.gcd(30,105,42), 3) + assert.ok( + math.associate( + math.lcm( + math.complex(2n,1n), math.complex(1n,1n), math.complex(0n,1n)), + math.complex(1n,3n))) + }) + }) diff --git a/test/core/_dependencyExtractor.mjs b/test/core/dependencyExtractor.mjs similarity index 89% rename from test/core/_dependencyExtractor.mjs rename to test/core/dependencyExtractor.mjs index 91e0e40..bc1683f 100644 --- a/test/core/_dependencyExtractor.mjs +++ b/test/core/dependencyExtractor.mjs @@ -1,5 +1,5 @@ import assert from 'assert' -import dependencyExtractor from '../../src/core/dependencyExtractor.mjs' +import {dependencyExtractor} from '../../src/core/extractors.mjs' describe('dependencyExtractor', () => { it('will record the keys of a destructuring function', () => { diff --git a/test/custom.mjs b/test/custom.mjs index 9fb66b1..6a399b0 100644 --- a/test/custom.mjs +++ b/test/custom.mjs @@ -8,6 +8,7 @@ import * as complex from '../src/complex/all.mjs' import * as complexAdd from '../src/complex/add.mjs' import * as complexNegate from '../src/complex/negate.mjs' import * as complexComplex from '../src/complex/complex.mjs' +import * as bigintAdd from '../src/bigint/add.mjs' import * as concreteSubtract from '../src/generic/subtract.concrete.mjs' import * as genericSubtract from '../src/generic/subtract.mjs' import extendToComplex from '../src/complex/extendToComplex.mjs' @@ -17,9 +18,10 @@ describe('A custom instance', () => { it("works when partially assembled", () => { bw.install(complex) // Not much we can call without any number types: - const i3 = {re: 0, im: 3} - assert.deepStrictEqual(bw.complex(0, 3), i3) - assert.deepStrictEqual(bw.chain(0).complex(3).value, i3) + assert.deepStrictEqual(bw.complex(undefined, undefined), undefined) + assert.deepStrictEqual( + bw.chain(undefined).complex(undefined).value, + undefined) // Don't have a way to negate things, for example: assert.throws(() => bw.negate(2), TypeError) }) @@ -33,7 +35,7 @@ describe('A custom instance', () => { assert.deepStrictEqual( bw.subtract(16, bw.add(3, bw.complex(0,4), 2)), math.complex(11, -4)) // note both instances coexist - assert.deepStrictEqual(bw.negate(math.complex(3, '8')).im, -8) + assert.deepStrictEqual(bw.negate(bw.complex(3, '8')).im, -8) }) it("can be assembled piecemeal", () => { @@ -112,4 +114,30 @@ describe('A custom instance', () => { math.complex(1n, -3n)) }) + it("instantiates templates correctly", () => { + const inst = new PocomathInstance('InstantiateTemplates') + inst.install(numberAdd) + inst.install({typeMerge: {'T,T': ({T}) => (t,u) => 'Merge to ' + T }}) + assert.strictEqual(inst.typeMerge(7,6.28), 'Merge to number') + assert.strictEqual(inst.typeMerge(7,6), 'Merge to NumInt') + assert.strictEqual(inst.typeMerge(7.35,6), 'Merge to number') + inst.install(complexAdd) + inst.install(complexComplex) + inst.install(bigintAdd) + assert.strictEqual( + inst.typeMerge(6n, inst.complex(3n, 2n)), + 'Merge to GaussianInteger') + assert.strictEqual( + inst.typeMerge(3, inst.complex(4.5,2.1)), + 'Merge to Complex') + // The following is the current behavior, since 3 converts to 3+0i + // which is technically the same Complex type as 3n+0ni. + // This should clear up when Complex is templatized + assert.strictEqual(inst.typeMerge(3, inst.complex(3n)), 'Merge to Complex') + // But types that truly cannot be merged should throw a TypeError + // Should add a variation of this with a more usual type once there is + // one not interconvertible with others... + inst.install(genericSubtract) + assert.throws(() => inst.typeMerge(3, undefined), TypeError) + }) }) diff --git a/test/tuple/_native.mjs b/test/tuple/_native.mjs new file mode 100644 index 0000000..2cf56d1 --- /dev/null +++ b/test/tuple/_native.mjs @@ -0,0 +1,114 @@ +import assert from 'assert' +import math from '../../src/pocomath.mjs' + +describe('tuple', () => { + it('can be created and provide its length', () => { + assert.strictEqual(math.length(math.tuple(3, 5.2, 2)), 3) + }) + + it('does not allow unification by converting consecutive arguments', () => { + assert.throws(() => math.tuple(3, 5.2, 2n), /TypeError.*unif/) + // Hence, the order matters in a slightly unfortunate way, + // but I think being a little ragged in these edge cases is OK: + assert.throws( + () => math.tuple(3, 2n, math.complex(5.2)), + /TypeError.*unif/) + assert.deepStrictEqual( + math.tuple(3, math.complex(2n), 5.2), + {elts: [math.complex(3), math.complex(2n), math.complex(5.2)]}) + }) + + it('can be tested for zero and equality', () => { + assert.strictEqual(math.isZero(math.tuple(0,1)), false) + assert.strictEqual(math.isZero(math.tuple(0n,0n,0n,0n)), true) + assert.strictEqual(math.isZero(math.tuple(0,0.001,0)), false) + assert.deepStrictEqual(math.complex(0,0), {re: 0, im:0}) + assert.strictEqual(math.isZero(math.tuple(0,math.complex(0,0))), true) + assert.strictEqual( + math.equal( + math.tuple(0,math.complex(0,0.1)), + math.complex(math.tuple(0,0), math.tuple(0,0.1))), + true) + assert.strictEqual( + math.equal(math.tuple(3n,2n), math.tuple(3,2)), + false) + }) + + it('supports addition', () => { + assert.deepStrictEqual( + math.add(math.tuple(3,4,5), math.tuple(2,1,0)), + math.tuple(5,5,5)) + assert.deepStrictEqual( + math.add(math.tuple(3.25,4.5,5), math.tuple(3,3)), + math.tuple(6.25,7.5,5)) + assert.deepStrictEqual( + math.add(math.tuple(math.complex(2,3), 7), math.tuple(4, 5, 6)), + math.tuple(math.complex(6,3), math.complex(12), math.complex(6))) + assert.deepStrictEqual( + math.add(math.tuple(5,6), 7), + math.tuple(12,6)) + assert.deepStrictEqual( + math.add(math.tuple(math.complex(5,4),6), 7), + math.tuple(math.complex(12,4),math.complex(6))) + }) + + it('supports subtraction', () => { + assert.deepStrictEqual( + math.subtract(math.tuple(3n,4n,5n), math.tuple(2n,1n,0n)), + math.tuple(1n,3n,5n)) + assert.throws( + () => math.subtract(math.tuple(5,6), math.tuple(7)), + /RangeError/) + }) + + it('makes a tuple of complex and conjugates it', () => { + const complexTuple = math.tuple( + math.complex(3,1), math.complex(4,2.2), math.complex(5,3)) + assert.deepStrictEqual( + math.complex(math.tuple(3,4,5), math.tuple(1,2.2,3)), + complexTuple) + assert.deepStrictEqual( + math.conjugate(complexTuple), + math.tuple(math.complex(3,-1), math.complex(4,-2.2), math.complex(5,-3))) + }) + + it('supports division', () => { + assert.deepStrictEqual( + math.divide(math.tuple(3,4,5),math.tuple(1,2,2)), + math.tuple(3,2,2.5)) + }) + + it('supports multiplication', () => { + assert.deepStrictEqual( + math.multiply(math.tuple(3,4,5), math.tuple(1,2,2)), + math.tuple(3,8,10)) + }) + + it('supports one and zero', () => { + assert.deepStrictEqual( + math.one(math.tuple(2n,3n,0n)), + math.tuple(1n,1n,1n)) + assert.deepStrictEqual( + math.zero(math.tuple(math.complex(5,2), 3.4)), + math.tuple(math.complex(0), math.complex(0))) + }) + + it('supports quotient and roundquotient', () => { + const bigTuple = math.tuple(1n,2n,3n,4n,5n) + const bigOnes = math.one(bigTuple) + const threes = math.add(bigOnes, bigOnes, bigOnes) + assert.deepStrictEqual( + math.quotient(bigTuple, threes), + math.tuple(0n, 0n, 1n, 1n, 1n)) + assert.deepStrictEqual( + math.roundquotient(bigTuple, threes), + math.tuple(0n, 1n, 1n, 1n, 2n)) + }) + + it('supports sqrt', () => { + assert.deepStrictEqual( + math.sqrt(math.tuple(4,-4,2.25)), + math.tuple(2, math.complex(0,2), 1.5)) + }) + +}) From 1444b9828ffb1c5663ee87856150a9c0b3bd5956 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sat, 6 Aug 2022 08:27:44 -0700 Subject: [PATCH 16/22] refactor(Complex): Now a template type! This means that the real and imaginary parts of a Complex must now be the same type. This seems like a real benefit: a Complex with a number real part and a bigint imaginary part does not seem sensible. Note that this is now straining typed-function in (at least) the following ways: (1) In this change, it was necessary to remove the logic that the square root of a negative number calls complex square root, which then calls back to the number square root in its algorithm. (This was creating a circular reference in the typed-function which the old implementation of Complex was somehow sidestepping.) (2) typed-function could not follow conversions that would be allowed by uninstantiated templates (e.g. number => Complex if the latter template has not been instantiated) and so the facility for instantiating a template was surfaced (and for example is called explicitly in the demo loader `extendToComplex`. Similarly, this necessitated making the unary signature of the `complex` conversion function explicit, rather than just via implicit conversion to Complex. (3) I find the order of implementations is mattering more in typed-function definitions, implying that typed-function's sorting algorithm is having trouble distinguishing alternatives. But otherwise, the conversion went quite smoothly and I think is a good demo of the power of this approach. And I expect that it will work even more smoothly if some of the underlying facilities (subtypes, template types) are integrated into typed-function. --- src/bigint/sqrt.mjs | 22 ++++++++---- src/complex/Types/Complex.mjs | 31 ++++++++-------- src/complex/abs.mjs | 5 ++- src/complex/absquare.mjs | 5 ++- src/complex/add.mjs | 22 +++--------- src/complex/associate.mjs | 16 ++++----- src/complex/complex.mjs | 6 +++- src/complex/conjugate.mjs | 5 ++- src/complex/equalTT.mjs | 33 ++++++++++------- src/complex/extendToComplex.mjs | 13 +++++++ src/complex/gcd.mjs | 16 +++++---- src/complex/invert.mjs | 13 ++++--- src/complex/isZero.mjs | 2 +- src/complex/multiply.mjs | 15 ++++---- src/complex/quotient.mjs | 4 ++- src/complex/roundquotient.mjs | 20 +++++------ src/complex/sqrt.mjs | 49 ++++++++++++------------- src/core/PocomathInstance.mjs | 59 ++++++++++++++++++++++--------- src/generic/reducingOperation.mjs | 1 + src/number/sqrt.mjs | 9 +++-- src/ops/floor.mjs | 4 +-- src/tuple/Types/Tuple.mjs | 7 +++- test/_pocomath.mjs | 12 +++---- test/complex/_all.mjs | 9 +++++ test/custom.mjs | 19 ++++++---- 25 files changed, 240 insertions(+), 157 deletions(-) diff --git a/src/bigint/sqrt.mjs b/src/bigint/sqrt.mjs index ad6bd67..4d34513 100644 --- a/src/bigint/sqrt.mjs +++ b/src/bigint/sqrt.mjs @@ -2,14 +2,18 @@ export * from './Types/bigint.mjs' import isqrt from 'bigint-isqrt' export const sqrt = { - bigint: ({config, complex, 'self(Complex)': complexSqrt}) => { + bigint: ({ + config, + 'complex(bigint,bigint)': cplx, + 'negate(bigint)': neg + }) => { if (config.predictable) { // Don't just return the constant isqrt here because the object // gets decorated with info that might need to be different // for different PocomathInstancss return b => isqrt(b) } - if (!complexSqrt) { + if (!cplx) { return b => { if (b >= 0n) { const trial = isqrt(b) @@ -19,12 +23,16 @@ export const sqrt = { } } return b => { - if (b >= 0n) { - const trial = isqrt(b) - if (trial * trial === b) return trial - return undefined + if (b === undefined) return undefined + let real = true + if (b < 0n) { + b = neg(b) + real = false } - return complexSqrt(complex(b)) + const trial = isqrt(b) + if (trial * trial !== b) return undefined + if (real) return trial + return cplx(0n, trial) } } } diff --git a/src/complex/Types/Complex.mjs b/src/complex/Types/Complex.mjs index e7644b9..0fa4107 100644 --- a/src/complex/Types/Complex.mjs +++ b/src/complex/Types/Complex.mjs @@ -1,30 +1,27 @@ import PocomathInstance from '../../core/PocomathInstance.mjs' -/* Use a plain object with keys re and im for a complex; note the components - * can be any type (for this proof-of-concept; in reality we'd want to - * insist on some numeric or scalar supertype). - */ -function isComplex(z) { - return z && typeof z === 'object' && 're' in z && 'im' in z -} - const Complex = new PocomathInstance('Complex') +// Base type that should generally not be used directly Complex.installType('Complex', { - test: isComplex, - from: { - number: x => ({re: x, im: 0}) - } + test: z => z && typeof z === 'object' && 're' in z && 'im' in z }) -Complex.installType('GaussianInteger', { - test: z => typeof z.re == 'bigint' && typeof z.im == 'bigint', - refines: 'Complex', +// Now the template type: Complex numbers are actually always homeogeneous +// in their component types. +Complex.installType('Complex', { + infer: ({typeOf, joinTypes}) => z => joinTypes([typeOf(z.re), typeOf(z.im)]), + test: testT => z => testT(z.re) && testT(z.im), from: { - bigint: x => ({re: x, im: 0n}) + T: t => ({re: t, im: t-t}), // hack: maybe need a way to call zero(T) + U: convert => u => { + const t = convert(u) + return ({re: t, im: t-t}) + }, + 'Complex': convert => cu => ({re: convert(cu.re), im: convert(cu.im)}) } }) Complex.promoteUnary = { - Complex: ({self,complex}) => z => complex(self(z.re), self(z.im)) + 'Complex': ({'self(T)': me, complex}) => z => complex(me(z.re), me(z.im)) } export {Complex} diff --git a/src/complex/abs.mjs b/src/complex/abs.mjs index 48d5a7b..47fc88d 100644 --- a/src/complex/abs.mjs +++ b/src/complex/abs.mjs @@ -1,5 +1,8 @@ export * from './Types/Complex.mjs' export const abs = { - Complex: ({sqrt, 'absquare(Complex)': absq}) => z => sqrt(absq(z)) + 'Complex': ({ + 'sqrt(T)': sqt, + 'absquare(Complex)': absq + }) => z => sqt(absq(z)) } diff --git a/src/complex/absquare.mjs b/src/complex/absquare.mjs index cfa1f30..913124b 100644 --- a/src/complex/absquare.mjs +++ b/src/complex/absquare.mjs @@ -1,5 +1,8 @@ export * from './Types/Complex.mjs' export const absquare = { - Complex: ({add, square}) => z => add(square(z.re), square(z.im)) + 'Complex': ({ + 'add(T,T)': plus, + 'square(T)': sqr + }) => z => plus(sqr(z.re), sqr(z.im)) } diff --git a/src/complex/add.mjs b/src/complex/add.mjs index 0c178d6..9afdd90 100644 --- a/src/complex/add.mjs +++ b/src/complex/add.mjs @@ -1,22 +1,8 @@ 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(number,number)': cplx - }) => (z,x) => cplx(addNum(z.re, x), z.im), - - 'Complex,bigint': ({ - 'self(bigint,bigint)': addBigInt, - 'complex(bigint,bigint)': cplx - }) => (z,x) => cplx(addBigInt(z.re, x), z.im), - - 'Complex,Complex': ({ - self, - complex - }) => (w,z) => complex(self(w.re, z.re), self(w.im, z.im)) + 'Complex,Complex': ({ + 'self(T,T)': me, + 'complex(T,T)': cplx + }) => (w,z) => cplx(me(w.re, z.re), me(w.im, z.im)) } diff --git a/src/complex/associate.mjs b/src/complex/associate.mjs index f3106b4..10c799c 100644 --- a/src/complex/associate.mjs +++ b/src/complex/associate.mjs @@ -2,16 +2,16 @@ export * from './Types/Complex.mjs' /* Returns true if w is z multiplied by a complex unit */ export const associate = { - 'Complex,Complex': ({ - 'multiply(Complex,Complex)': times, - 'equalTT(Complex,Complex)': eq, - zero, - one, - complex, - 'negate(Complex)': neg + 'Complex,Complex': ({ + 'multiply(Complex,Complex)': times, + 'equalTT(Complex,Complex)': eq, + 'zero(T)': zr, + 'one(T)': uno, + 'complex(T,T)': cplx, + 'negate(Complex)': neg }) => (w,z) => { if (eq(w,z) || eq(w,neg(z))) return true - const ti = times(z, complex(zero(z.re), one(z.im))) + const ti = times(z, cplx(zr(z.re), uno(z.im))) return eq(w,ti) || eq(w,neg(ti)) } } diff --git a/src/complex/complex.mjs b/src/complex/complex.mjs index c34434c..a5a24f5 100644 --- a/src/complex/complex.mjs +++ b/src/complex/complex.mjs @@ -12,5 +12,9 @@ export const complex = { 'undefined,undefined': () => (u, v) => u, 'T,T': () => (x, y) => ({re: x, im: y}), /* Take advantage of conversions in typed-function */ - Complex: () => z => z + // 'Complex': () => z => z + /* But help out because without templates built in to typed-function, + * type inference turns out to be too hard + */ + 'T': ({'zero(T)': zr}) => x => ({re: x, im: zr(x)}) } diff --git a/src/complex/conjugate.mjs b/src/complex/conjugate.mjs index 3139506..b94495d 100644 --- a/src/complex/conjugate.mjs +++ b/src/complex/conjugate.mjs @@ -1,6 +1,9 @@ export * from './Types/Complex.mjs' export const conjugate = { - Complex: ({negate, complex}) => z => complex(z.re, negate(z.im)) + 'Complex': ({ + 'negate(T)': neg, + 'complex(T,T)': cplx + }) => z => cplx(z.re, neg(z.im)) } diff --git a/src/complex/equalTT.mjs b/src/complex/equalTT.mjs index 43fe0d1..6899aa0 100644 --- a/src/complex/equalTT.mjs +++ b/src/complex/equalTT.mjs @@ -1,19 +1,26 @@ export * from './Types/Complex.mjs' export const equalTT = { - 'Complex,number': ({ - 'isZero(number)': isZ, - 'self(number,number)': eqNum - }) => (z, x) => eqNum(z.re, x) && isZ(z.im), + 'Complex,Complex': ({ + 'self(T,T)': me + }) => (w,z) => me(w.re, z.re) && me(w.im, z.im), + // NOTE: Although I do not understand exactly why, with typed-function@3.0's + // matching algorithm, the above template must come first to ensure the + // most specific match to a template call. I.e, if one of the below + // comes first, a call with two complex numbers can match via conversions + // with (Complex>, Complex) (!, hopefully in some + // future iteration typed-function will be smart enough to prefer + // Complex, Complex. Possibly the problem is in Pocomath's bolted-on + // type resolution and the difficulty will go away when features are moved into + // typed-function. + 'Complex,T': ({ + 'isZero(T)': isZ, + 'self(T,T)': eqReal + }) => (z, x) => eqReal(z.re, x) && isZ(z.im), - 'Complex,bigint': ({ - 'isZero(bigint)': isZ, - 'self(bigint,bigint)': eqBigInt - }) => (z, b) => eqBigInt(z.re, b) && isZ(z.im), + 'T,Complex': ({ + 'isZero(T)': isZ, + 'self(T,T)': eqReal + }) => (b, z) => eqReal(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/extendToComplex.mjs b/src/complex/extendToComplex.mjs index 21e1a29..62e2e88 100644 --- a/src/complex/extendToComplex.mjs +++ b/src/complex/extendToComplex.mjs @@ -15,4 +15,17 @@ export default async function extendToComplex(pmath) { // Guess it wasn't a method available in complex; no worries } } + // Since extension to complex was specifically requested, instantiate + // all of the templates so that the associated type conversions will + // be available to make function calls work immediately: + for (const baseType in pmath.Types) { + if (baseType in pmath.Templates || baseType.includes('<')) { + continue // don't mess with templates + } + const ignore = new Set(['undefined', 'any', 'T', 'ground']) + if (ignore.has(baseType)) continue + // (What we really want is a check for "numeric" types but we don't + // have that concept (yet?)). If we did, we'd instantiate just for those... + pmath.instantiateTemplate('Complex', baseType) + } } diff --git a/src/complex/gcd.mjs b/src/complex/gcd.mjs index 238d811..30b7dad 100644 --- a/src/complex/gcd.mjs +++ b/src/complex/gcd.mjs @@ -2,16 +2,20 @@ import PocomathInstance from '../core/PocomathInstance.mjs' import * as Complex from './Types/Complex.mjs' import gcdType from '../generic/gcdType.mjs' +const gcdComplexRaw = {} +Object.assign(gcdComplexRaw, gcdType('Complex')) +Object.assign(gcdComplexRaw, gcdType('Complex')) const imps = { - gcdGIRaw: gcdType('GaussianInteger'), + gcdComplexRaw, gcd: { // Only return gcds with positive real part - 'GaussianInteger,GaussianInteger': ({ - 'gcdGIRaw(GaussianInteger,GaussianInteger)': gcdRaw, - 'sign(bigint)': sgn, - 'negate(GaussianInteger)': neg + 'Complex,Complex': ({ + 'gcdComplexRaw(Complex,Complex)': gcdRaw, + 'sign(T)': sgn, + 'one(T)': uno, + 'negate(Complex)': neg }) => (z,m) => { const raw = gcdRaw(z, m) - if (sgn(raw.re) === 1n) return raw + if (sgn(raw.re) === uno(raw.re)) return raw return neg(raw) } } diff --git a/src/complex/invert.mjs b/src/complex/invert.mjs index dc7779a..2f68e43 100644 --- a/src/complex/invert.mjs +++ b/src/complex/invert.mjs @@ -1,9 +1,14 @@ export * from './Types/Complex.mjs' export const invert = { - Complex: ({conjugate, absquare, complex, divide}) => z => { - const c = conjugate(z) - const d = absquare(z) - return complex(divide(c.re, d), divide(c.im, d)) + 'Complex': ({ + 'conjugate(Complex)': conj, + 'absquare(Complex)': asq, + 'complex(T,T)': cplx, + 'divide(T,T)': div + }) => z => { + const c = conj(z) + const d = asq(z) + return cplx(div(c.re, d), div(c.im, d)) } } diff --git a/src/complex/isZero.mjs b/src/complex/isZero.mjs index 1eaad7f..01a2f51 100644 --- a/src/complex/isZero.mjs +++ b/src/complex/isZero.mjs @@ -1,5 +1,5 @@ export * from './Types/Complex.mjs' export const isZero = { - Complex: ({self}) => z => self(z.re) && self(z.im) + 'Complex': ({'self(T)': me}) => z => me(z.re) && me(z.im) } diff --git a/src/complex/multiply.mjs b/src/complex/multiply.mjs index e1b46fe..e059a91 100644 --- a/src/complex/multiply.mjs +++ b/src/complex/multiply.mjs @@ -1,14 +1,15 @@ export * from './Types/Complex.mjs' export const multiply = { - 'Complex,Complex': ({ - 'complex(any,any)': cplx, - add, - subtract, - self + 'Complex,Complex': ({ + 'complex(T,T)': cplx, + 'add(T,T)': plus, + 'subtract(T,T)': sub, + 'self(T,T)': me, + 'conjugate(T)': conj // makes quaternion multiplication work }) => (w,z) => { return cplx( - subtract(self(w.re, z.re), self(w.im, z.im)), - add(self(w.re, z.im), self(w.im, z.re))) + sub(me(w.re, z.re), me(conj(w.im), z.im)), + plus(me(conj(w.re), z.im), me(w.im, z.re))) } } diff --git a/src/complex/quotient.mjs b/src/complex/quotient.mjs index 20090e0..32299ca 100644 --- a/src/complex/quotient.mjs +++ b/src/complex/quotient.mjs @@ -1,5 +1,7 @@ export * from './roundquotient.mjs' export const quotient = { - 'Complex,Complex': ({roundquotient}) => (w,z) => roundquotient(w,z) + 'Complex,Complex': ({ + 'roundquotient(Complex,Complex)': rq + }) => (w,z) => rq(w,z) } diff --git a/src/complex/roundquotient.mjs b/src/complex/roundquotient.mjs index c9b2cbc..5c25765 100644 --- a/src/complex/roundquotient.mjs +++ b/src/complex/roundquotient.mjs @@ -1,17 +1,17 @@ export * from './Types/Complex.mjs' export const roundquotient = { - 'Complex,Complex': ({ - 'isZero(Complex)': isZ, - conjugate, - 'multiply(Complex,Complex)': mult, - absquare, - self, - complex + 'Complex,Complex': ({ + 'isZero(Complex)': isZ, + 'conjugate(Complex)': conj, + 'multiply(Complex,Complex)': mult, + 'absquare(Complex)': asq, + 'self(T,T)': me, + 'complex(T,T)': cplx }) => (n,d) => { if (isZ(d)) return d - const cnum = mult(n, conjugate(d)) - const dreal = absquare(d) - return complex(self(cnum.re, dreal), self(cnum.im, dreal)) + const cnum = mult(n, conj(d)) + const dreal = asq(d) + return cplx(me(cnum.re, dreal), me(cnum.im, dreal)) } } diff --git a/src/complex/sqrt.mjs b/src/complex/sqrt.mjs index 3557c6d..d60ed6a 100644 --- a/src/complex/sqrt.mjs +++ b/src/complex/sqrt.mjs @@ -1,38 +1,39 @@ export * from './Types/Complex.mjs' export const sqrt = { - Complex: ({ + 'Complex': ({ config, - isZero, - sign, - one, - add, - complex, - multiply, - self, - divide, - 'abs(Complex)': abs, - subtract + 'isZero(T)': isZ, + 'sign(T)': sgn, + 'one(T)': uno, + 'add(T,T)': plus, + 'complex(T)': cplxU, + 'complex(T,T)': cplxB, + 'multiply(T,T)': mult, + 'self(T)': me, + 'divide(T,T)': div, + 'abs(Complex)': absC, + 'subtract(T,T)': sub }) => { if (config.predictable) { return z => { - const reOne = one(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))), - self(divide(subtract(abs(z),z.re), reTwo)) + const reOne = uno(z.re) + if (isZ(z.im) && sgn(z.re) === reOne) return cplxU(me(z.re)) + const reTwo = plus(reOne, reOne) + return cplxB( + mult(sgn(z.im), me(div(plus(absC(z),z.re), reTwo))), + me(div(sub(absC(z),z.re), reTwo)) ) } } return z => { - const reOne = one(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))), - self(divide(subtract(abs(z),z.re), reTwo)) - ) + const reOne = uno(z.re) + if (isZ(z.im) && sgn(z.re) === reOne) return me(z.re) + const reTwo = plus(reOne, reOne) + const reSqrt = me(div(plus(absC(z),z.re), reTwo)) + const imSqrt = me(div(sub(absC(z),z.re), reTwo)) + if (reSqrt === undefined || imSqrt === undefined) return undefined + return cplxB(mult(sgn(z.im), reSqrt), imSqrt) } } } diff --git a/src/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index 05636d4..b01c41f 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -28,6 +28,7 @@ export default class PocomathInstance { 'importDependencies', 'install', 'installType', + 'instantiateTemplate', 'joinTypes', 'name', 'self', @@ -436,7 +437,12 @@ export default class PocomathInstance { } return } - // Nothing actually happens until we match a template parameter + // update the typeOf function + const imp = {} + imp[type] = {uses: new Set(['T']), does: ({T}) => () => `${base}<${T}>`} + this._installFunctions({typeOf: imp}) + + // Nothing else actually happens until we match a template parameter this.Templates[base] = {type, spec} } @@ -767,8 +773,9 @@ export default class PocomathInstance { const subsig = substituteInSig( needsig, theTemplateParam, instantiateFor) let resname = simplifiedDep - if (resname === 'self') resname = name - innerRefs[dep] = self._pocoresolve(resname, subsig) + if (resname == 'self') resname = name + innerRefs[dep] = self._pocoresolve( + resname, subsig, refs[simplifiedDep]) } else { innerRefs[dep] = refs[simplifiedDep] } @@ -782,7 +789,7 @@ export default class PocomathInstance { this._addTFimplementation( tf_imps, signature, {uses: outerUses, does: patch}) } - this._correctPartialSelfRefs(tf_imps) + this._correctPartialSelfRefs(name, tf_imps) const tf = this._typed(name, tf_imps) Object.defineProperty(this, name, {configurable: true, value: tf}) return tf @@ -845,7 +852,7 @@ export default class PocomathInstance { } else { // can bundle up func, and grab its signature if need be let destination = this[func] - if (needsig) { + if (destination &&needsig) { destination = this._pocoresolve(func, needsig) } refs[dep] = destination @@ -878,7 +885,7 @@ export default class PocomathInstance { imps[signature] = does(refs) } - _correctPartialSelfRefs(imps) { + _correctPartialSelfRefs(name, imps) { for (const aSignature in imps) { if (!(imps[aSignature].deferred)) continue const part_self_references = imps[aSignature].psr @@ -894,7 +901,7 @@ export default class PocomathInstance { // No exact match, try to get one that matches with // subtypes since the whole conversion thing in typed-function // is too complicated to reproduce - const foundSig = this._findSubtypeImpl(imps, neededSig) + const foundSig = this._findSubtypeImpl(name, imps, neededSig) if (foundSig) { corrected_self_references.push(foundSig) } else { @@ -936,7 +943,7 @@ export default class PocomathInstance { } const resultingTypes = new Set() for (const iType of instantiations) { - const resultType = this._maybeAddTemplateType(base, iType) + const resultType = this.instantiateTemplate(base, iType) if (resultType) resultingTypes.add(resultType) } return resultingTypes @@ -946,7 +953,7 @@ export default class PocomathInstance { * instantiator to the Types of this instance, if it hasn't happened already. * Returns the name of the type if added, false otherwise. */ - _maybeAddTemplateType(base, instantiator) { + instantiateTemplate(base, instantiator) { const wantsType = `${base}<${instantiator}>` if (wantsType in this.Types) return false // OK, need to generate the type from the template @@ -956,7 +963,7 @@ export default class PocomathInstance { const template = this.Templates[base].spec if (!template) { throw new Error( - `Implementor error in _maybeAddTemplateType ${base} ${instantiator}`) + `Implementor error in instantiateTemplate(${base}, ${instantiator})`) } const instantiatorSpec = this.Types[instantiator] let beforeTypes = [] @@ -1010,7 +1017,7 @@ export default class PocomathInstance { return wantsType } - _findSubtypeImpl(imps, neededSig) { + _findSubtypeImpl(name, imps, neededSig) { if (neededSig in imps) return neededSig let foundSig = false const typeList = typeListOfSignature(neededSig) @@ -1047,6 +1054,13 @@ export default class PocomathInstance { || this._subtypes[otherType].has(myType)) { continue } + if (otherType in this.Templates) { + if (this.instantiateTemplate(otherType, myType)) { + let dummy + dummy = this[name] // for side effects + return this._findSubtypeImpl(name, this._imps[name], neededSig) + } + } allMatch = false break } @@ -1058,17 +1072,28 @@ export default class PocomathInstance { return foundSig } - _pocoresolve(name, sig) { - const typedfunc = this[name] + _pocoresolve(name, sig, typedFunction) { + if (!this._typed.isTypedFunction(typedFunction)) { + typedFunction = this[name] + } let result = undefined try { - result = this._typed.find(typedfunc, sig, {exact: true}) + result = this._typed.find(typedFunction, sig, {exact: true}) } catch { } if (result) return result - const foundsig = this._findSubtypeImpl(this._imps[name], sig) - if (foundsig) return this._typed.find(typedfunc, foundsig) - return this._typed.find(typedfunc, sig) + const foundsig = this._findSubtypeImpl(name, this._imps[name], sig) + if (foundsig) return this._typed.find(typedFunction, foundsig) + // Make sure bundle is up-to-date: + typedFunction = this[name] + try { + result = this._typed.find(typedFunction, sig) + } catch { + } + if (result) return result + // total punt, revert to typed-function resolution on every call; + // hopefully this happens rarely: + return typedFunction } } diff --git a/src/generic/reducingOperation.mjs b/src/generic/reducingOperation.mjs index 101a8ec..e29baf1 100644 --- a/src/generic/reducingOperation.mjs +++ b/src/generic/reducingOperation.mjs @@ -4,6 +4,7 @@ export const reducingOperation = { 'undefined': () => u => u, 'undefined,...any': () => (u, rest) => u, 'any,undefined': () => (x, u) => u, + 'undefined,undefined': () => (u,v) => u, any: () => x => x, 'any,any,...any': ({ self diff --git a/src/number/sqrt.mjs b/src/number/sqrt.mjs index f6cc459..3017e82 100644 --- a/src/number/sqrt.mjs +++ b/src/number/sqrt.mjs @@ -1,14 +1,17 @@ export * from './Types/number.mjs' export const sqrt = { - number: ({config, complex, 'self(Complex)': complexSqrt}) => { - if (config.predictable || !complexSqrt) { + number: ({ + config, + 'complex(number,number)': cplx, + 'negate(number)': neg}) => { + if (config.predictable || !cplx) { return n => isNaN(n) ? NaN : Math.sqrt(n) } return n => { if (isNaN(n)) return NaN if (n >= 0) return Math.sqrt(n) - return complexSqrt(complex(n)) + return cplx(0, Math.sqrt(neg(n))) } } } diff --git a/src/ops/floor.mjs b/src/ops/floor.mjs index e8897e8..71c8e70 100644 --- a/src/ops/floor.mjs +++ b/src/ops/floor.mjs @@ -7,7 +7,7 @@ import {Complex} from '../complex/Types/Complex.mjs' export const floor = { bigint: () => x => x, NumInt: () => x => x, // Because Pocomath isn't part of typed-function, or - GaussianInteger: () => x => x, // at least have access to the real + 'Complex': () => x => x, // at least have access to the real // typed-function parse, we unfortunately can't coalesce these into one // entry with type `bigint|NumInt|GaussianInteger` because they couldn't // be separately activated then @@ -17,7 +17,7 @@ export const floor = { return Math.floor(n) }, - Complex: Complex.promoteUnary.Complex, + 'Complex': Complex.promoteUnary['Complex'], // OK to include a type totally not in Pocomath yet, it'll never be // activated. diff --git a/src/tuple/Types/Tuple.mjs b/src/tuple/Types/Tuple.mjs index 0c6c0ae..d76d147 100644 --- a/src/tuple/Types/Tuple.mjs +++ b/src/tuple/Types/Tuple.mjs @@ -32,7 +32,12 @@ Tuple.installType('Tuple', { }) Tuple.promoteUnary = { - 'Tuple': ({'self(T)': me, tuple}) => t => tuple(...(t.elts.map(me))) + 'Tuple': ({ + 'self(T)': me, + tuple + }) => t => tuple(...(t.elts.map(x => me(x)))) // NOTE: this must use + // the inner arrow function to drop additional arguments that Array.map + // supplies, as otherwise the wrong signature of `me` might be used. } Tuple.promoteBinaryUnary = { diff --git a/test/_pocomath.mjs b/test/_pocomath.mjs index 1d7d1e9..38df18b 100644 --- a/test/_pocomath.mjs +++ b/test/_pocomath.mjs @@ -16,8 +16,8 @@ describe('The default full pocomath instance "math"', () => { 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') + assert.strictEqual(math.typeOf({re: 15n, im: -2n}), 'Complex') + assert.strictEqual(math.typeOf({re: 6.28, im: 2.72}), 'Complex') }) it('can subtract numbers', () => { @@ -105,11 +105,9 @@ describe('The default full pocomath instance "math"', () => { it('calculates multi-way gcds and lcms', () => { assert.strictEqual(math.gcd(30,105,42), 3) - assert.ok( - math.associate( - math.lcm( - math.complex(2n,1n), math.complex(1n,1n), math.complex(0n,1n)), - math.complex(1n,3n))) + const gaussianLCM = math.lcm( + math.complex(2n,1n), math.complex(1n,1n), math.complex(0n,1n)) + assert.strictEqual(math.associate(gaussianLCM, math.complex(1n,3n)), true) }) }) diff --git a/test/complex/_all.mjs b/test/complex/_all.mjs index e486fc3..801aa99 100644 --- a/test/complex/_all.mjs +++ b/test/complex/_all.mjs @@ -49,6 +49,15 @@ describe('complex', () => { assert.deepStrictEqual( math.gcd(math.complex(53n, 56n), math.complex(47n, -13n)), math.complex(4n, 5n)) + // And now works for NumInt, too! + assert.deepStrictEqual( + math.gcd(math.complex(53,56), math.complex(47, -13)), + math.complex(4, 5)) + // But properly fails for general complex + assert.throws( + () => math.gcd(math.complex(5.3,5.6), math.complex(4.7, -1.3)), + TypeError + ) }) it('computes floor', () => { diff --git a/test/custom.mjs b/test/custom.mjs index 6a399b0..f6612ef 100644 --- a/test/custom.mjs +++ b/test/custom.mjs @@ -3,12 +3,14 @@ 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 * as numberZero from '../src/number/zero.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' import * as complexComplex from '../src/complex/complex.mjs' import * as bigintAdd from '../src/bigint/add.mjs' +import * as bigintZero from '../src/bigint/zero.mjs' import * as concreteSubtract from '../src/generic/subtract.concrete.mjs' import * as genericSubtract from '../src/generic/subtract.mjs' import extendToComplex from '../src/complex/extendToComplex.mjs' @@ -54,9 +56,9 @@ describe('A custom instance', () => { math.complex(-5, -1)) // And now floor has been activated for Complex as well, since the type // is present - assert.deepStrictEqual( - pm.floor(math.complex(1.9, 0)), - math.complex(1)) + const fracComplex = math.complex(1.9, 0) + const intComplex = math.complex(1) + assert.deepStrictEqual(pm.floor(fracComplex), intComplex) // And the chain functions refresh themselves: assert.deepStrictEqual( pm.chain(5).add(pm.chain(0).complex(7).value).value, math.complex(5,7)) @@ -65,10 +67,11 @@ describe('A custom instance', () => { it("can defer definition of (even used) types", () => { const dt = new PocomathInstance('Deferred Types') dt.install(numberAdd) + dt.install(numberZero) // for promoting numbers to complex, to fill in im dt.install({times: { 'number,number': () => (m,n) => m*n, - 'Complex,Complex': ({complex}) => (w,z) => { - return complex(w.re*z.re - w.im*z.im, w.re*z.im + w.im*z.re) + 'Complex,Complex': ({'complex(T,T)': cplx}) => (w,z) => { + return cplx(w.re*z.re - w.im*z.im, w.re*z.im + w.im*z.re) } }}) // complex type not present but should still be able to add numbers: @@ -82,6 +85,7 @@ describe('A custom instance', () => { it("can selectively import in cute ways", async function () { const cherry = new PocomathInstance('cherry') cherry.install(numberAdd) + cherry.install(numberZero) // for complex promotion await extendToComplex(cherry) cherry.install({add: genericAdd}) /* Now we have an instance that supports addition for number and complex @@ -124,12 +128,13 @@ describe('A custom instance', () => { inst.install(complexAdd) inst.install(complexComplex) inst.install(bigintAdd) + inst.install(bigintZero) // for complex promotion assert.strictEqual( inst.typeMerge(6n, inst.complex(3n, 2n)), - 'Merge to GaussianInteger') + 'Merge to Complex') assert.strictEqual( inst.typeMerge(3, inst.complex(4.5,2.1)), - 'Merge to Complex') + 'Merge to Complex') // The following is the current behavior, since 3 converts to 3+0i // which is technically the same Complex type as 3n+0ni. // This should clear up when Complex is templatized From e26df5f4fc253eade722eb02ab8caa49dc06934c Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sat, 6 Aug 2022 20:13:50 -0700 Subject: [PATCH 17/22] feat(quaternion): Add convenience quaternion creator function Even in the setup just prior to this commit, a quaternion with entries of type `number` is simply a `Complex>` So if we provide a convenience wrapper to create sucha thing, we instantly have a quaternion data type. All of the operations come for "free" if they were properly defined for the `Complex` template. Multiplication already was, `abs` needed a little tweak, but there is absolutely no "extra" code to support quaternions. (This commit does not go through and check all arithmetic functions for proper operation and tweak those that still need some generalization.) Note that with the recursive template instantiation, a limit had to be placed on template instantiation depth. The limit moves deeper as actual arguments that are deeper nested instantiations are seen, so as long as one doesn't immediately invoke a triply-nested template, for example, the limit will never prevent an actual computation. It just prevents a runaway in the types that Pocomath thinks it needs to know about. (Basically before, using the quaternion creator would produce `Complex>`. Then when you called it again, Pocomath would think "Maybe I will need `Complex>>`?!" and create that, even though it had never seen that, and then another level next time, and so on. The limit just stops this progression one level beyond any nesting depth that's actually been observed. --- src/bigint/absquare.mjs | 6 ++++ src/bigint/native.mjs | 1 + src/complex/abs.mjs | 10 +++--- src/complex/absquare.mjs | 7 ++-- src/complex/native.mjs | 1 + src/complex/quaternion.mjs | 5 +++ src/core/PocomathInstance.mjs | 64 ++++++++++++++++++++++++++++++----- src/number/absquare.mjs | 6 ++++ src/number/native.mjs | 1 + test/complex/_all.mjs | 19 +++++++++++ 10 files changed, 104 insertions(+), 16 deletions(-) create mode 100644 src/bigint/absquare.mjs create mode 100644 src/complex/quaternion.mjs create mode 100644 src/number/absquare.mjs diff --git a/src/bigint/absquare.mjs b/src/bigint/absquare.mjs new file mode 100644 index 0000000..4c2040a --- /dev/null +++ b/src/bigint/absquare.mjs @@ -0,0 +1,6 @@ +export * from './Types/bigint.mjs' + +/* Absolute value squared */ +export const absquare = { + bigint: ({'square(bigint)': sqb}) => b => sqb(b) +} diff --git a/src/bigint/native.mjs b/src/bigint/native.mjs index 6458912..6cc76d4 100644 --- a/src/bigint/native.mjs +++ b/src/bigint/native.mjs @@ -3,6 +3,7 @@ import {identity} from '../generic/identity.mjs' export * from './Types/bigint.mjs' +export {absquare} from './absquare.mjs' export {add} from './add.mjs' export {compare} from './compare.mjs' export const conjugate = {bigint: () => identity} diff --git a/src/complex/abs.mjs b/src/complex/abs.mjs index 47fc88d..536b8b4 100644 --- a/src/complex/abs.mjs +++ b/src/complex/abs.mjs @@ -1,8 +1,10 @@ export * from './Types/Complex.mjs' export const abs = { - 'Complex': ({ - 'sqrt(T)': sqt, - 'absquare(Complex)': absq - }) => z => sqt(absq(z)) + 'Complex': ({ + sqrt, // Calculation of the type needed in the square root (the + // underlying numeric type of T, whatever T is, is beyond Pocomath's + // (current) template abilities, so punt and just do full resolution + 'absquare(Complex)': absq + }) => z => sqrt(absq(z)) } diff --git a/src/complex/absquare.mjs b/src/complex/absquare.mjs index 913124b..bb4677f 100644 --- a/src/complex/absquare.mjs +++ b/src/complex/absquare.mjs @@ -2,7 +2,8 @@ export * from './Types/Complex.mjs' export const absquare = { 'Complex': ({ - 'add(T,T)': plus, - 'square(T)': sqr - }) => z => plus(sqr(z.re), sqr(z.im)) + add, // Calculation of exact type needed in add (underlying numeric of T) + // is (currently) too involved for Pocomath + 'self(T)': absq + }) => z => add(absq(z.re), absq(z.im)) } diff --git a/src/complex/native.mjs b/src/complex/native.mjs index 420ce88..93c26e4 100644 --- a/src/complex/native.mjs +++ b/src/complex/native.mjs @@ -12,6 +12,7 @@ export {invert} from './invert.mjs' export {isZero} from './isZero.mjs' export {multiply} from './multiply.mjs' export {negate} from './negate.mjs' +export {quaternion} from './quaternion.mjs' export {quotient} from './quotient.mjs' export {roundquotient} from './roundquotient.mjs' export {sqrt} from './sqrt.mjs' diff --git a/src/complex/quaternion.mjs b/src/complex/quaternion.mjs new file mode 100644 index 0000000..4f35d30 --- /dev/null +++ b/src/complex/quaternion.mjs @@ -0,0 +1,5 @@ +export * from './Types/Complex.mjs' + +export const quaternion = { + 'T,T,T,T': ({complex}) => (r,i,j,k) => complex(complex(r,j), complex(i,k)) +} diff --git a/src/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index b01c41f..bb013d1 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -64,6 +64,7 @@ export default class PocomathInstance { */ this._priorTypes = {} this._seenTypes = new Set() // all types that have occurred in a signature + this._maxDepthSeen = 1 // deepest template nesting we've actually encountered this._invalid = new Set() // methods that are currently invalid this._config = {predictable: false, epsilon: 1e-12} const self = this @@ -336,6 +337,7 @@ export default class PocomathInstance { let nextSuper = type while (nextSuper) { if (this._priorTypes[nextSuper].has(from)) break + if (from === nextSuper) break this._typed.addConversion( {from, to: nextSuper, convert: spec.from[from]}) this._invalidateDependents(':' + nextSuper) @@ -360,12 +362,16 @@ export default class PocomathInstance { } let nextSuper = to while (nextSuper) { - this._typed.addConversion({ - from: type, - to: nextSuper, - convert: this.Types[to].from[fromtype] - }) - this._invalidateDependents(':' + nextSuper) + if (type === nextSuper) break + try { // may already be a conversion, and no way to ask + this._typed.addConversion({ + from: type, + to: nextSuper, + convert: this.Types[to].from[fromtype] + }) + this._invalidateDependents(':' + nextSuper) + } catch { + } this._priorTypes[nextSuper].add(type) nextSuper = this.Types[nextSuper].refines } @@ -609,6 +615,14 @@ export default class PocomathInstance { substituteInSig(rawSignature, theTemplateParam, instType) /* Don't override an explicit implementation: */ if (signature in imps) continue + /* Don't go too deep */ + let maxdepth = 0 + for (const argType in typeListOfSignature(signature)) { + const depth = argType.split('<').length + if (depth > maxdepth) maxdepth = depth + } + if (maxdepth > this._maxDepthSeen + 1) continue + /* All right, go ahead and instantiate */ const uses = new Set() for (const dep of behavior.uses) { if (this._templateParam(dep)) continue @@ -718,6 +732,10 @@ export default class PocomathInstance { + 'supertype of at least one of them') } } + const depth = instantiateFor.split('<').length + if (depth > self._maxDepthSeen) { + self._maxDepthSeen = depth + } /* Generate the list of actual wanted types */ const wantTypes = parTypes.map(type => substituteInSig( type, theTemplateParam, instantiateFor)) @@ -790,6 +808,28 @@ export default class PocomathInstance { tf_imps, signature, {uses: outerUses, does: patch}) } this._correctPartialSelfRefs(name, tf_imps) + // Make sure we have all of the needed (template) types; and if they + // can't be added (because they have been instantiated too deep), + // ditch the signature: + const badSigs = new Set() + for (const sig in tf_imps) { + for (const type of typeListOfSignature(sig)) { + if (type.includes('<')) { + // it's a template type, turn it into a template and an arg + let base = type.split('<',1)[0] + const arg = type.slice(base.length+1, -1) + if (base.slice(0,3) === '...') { + base = base.slice(3) + } + if (this.instantiateTemplate(base, arg) === undefined) { + badSigs.add(sig) + } + } + } + } + for (const badSig of badSigs) { + delete tf_imps[badSig] + } const tf = this._typed(name, tf_imps) Object.defineProperty(this, name, {configurable: true, value: tf}) return tf @@ -928,8 +968,8 @@ export default class PocomathInstance { * in the instance. */ _ensureTemplateTypes(template, type) { - let [base, arg] = template.split('<', 2) - arg = arg.slice(0,-1) + const base = template.split('<', 1)[0] + const arg = template.slice(base.length + 1, -1) if (!arg) { throw new Error( 'Implementation error in _ensureTemplateTypes', template, type) @@ -951,9 +991,15 @@ export default class PocomathInstance { /* Maybe add the instantiation of template type base with argument tyoe * instantiator to the Types of this instance, if it hasn't happened already. - * Returns the name of the type if added, false otherwise. + * Returns the name of the type if added, false if it was already there, + * and undefined if the type is declined (because of being nested too deep). */ instantiateTemplate(base, instantiator) { + const depth = instantiator.split('<').length + if (depth > this._maxDepthSeen ) { + // don't bother with types much deeper thant we have seen + return undefined + } const wantsType = `${base}<${instantiator}>` if (wantsType in this.Types) return false // OK, need to generate the type from the template diff --git a/src/number/absquare.mjs b/src/number/absquare.mjs new file mode 100644 index 0000000..d6ab55a --- /dev/null +++ b/src/number/absquare.mjs @@ -0,0 +1,6 @@ +export * from './Types/number.mjs' + +/* Absolute value squared */ +export const absquare = { + number: ({'square(number)': sqn}) => n => sqn(n) +} diff --git a/src/number/native.mjs b/src/number/native.mjs index d095574..6746408 100644 --- a/src/number/native.mjs +++ b/src/number/native.mjs @@ -4,6 +4,7 @@ import {identity} from '../generic/identity.mjs' export * from './Types/number.mjs' export {abs} from './abs.mjs' +export {absquare} from './absquare.mjs' export {add} from './add.mjs' export {compare} from './compare.mjs' export const conjugate = {number: () => identity} diff --git a/test/complex/_all.mjs b/test/complex/_all.mjs index 801aa99..531a28d 100644 --- a/test/complex/_all.mjs +++ b/test/complex/_all.mjs @@ -68,4 +68,23 @@ describe('complex', () => { assert.strictEqual(math.floor(gi), gi) // literally a no-op }) + it('performs rudimentary quaternion calculations', () => { + const q0 = math.quaternion(1, 0, 1, 0) + const q1 = math.quaternion(1, 0.5, 0.5, 0.75) + assert.deepStrictEqual( + q1, + math.complex(math.complex(1, 0.5), math.complex(0.5, 0.75))) + assert.deepStrictEqual( + math.add(q0,q1), + math.quaternion(2, 0.5, 1.5, 0.75)) + assert.deepStrictEqual( + math.multiply(q0, q1), + math.quaternion(0.5, 1.25, 1.5, 0.25)) + assert.deepStrictEqual( + math.multiply(q0, math.quaternion(2, 1, 0.1, 0.1)), + math.quaternion(1.9, 1.1, 2.1, -0.9)) + assert.strictEqual(math.abs(q0), Math.sqrt(2)) + assert.strictEqual(math.abs(q1), Math.sqrt(33)/4) + }) + }) From 199ffd26540fd2add38f4072d00033aedb623406 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sun, 7 Aug 2022 03:34:39 +0000 Subject: [PATCH 18/22] doc(README): Add brief notes about new template facilities --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index b97781d..ea194c1 100644 --- a/README.md +++ b/README.md @@ -25,3 +25,5 @@ Hopefully this shows promise. It is an evolution of the concept first prototyped Note that Pocomath allows one implementation to depend just on a specific signature of another function, for efficiency's sake (if for example 'bar(Matrix)' knows it will only call 'foo(Matrix)', it avoids another type-dispatch). That capability is used in sqrt, for example. Pocomath also lazily reloads operations that depend on the config when that changes, and if an operation has a signature mentioning an undefined type, that signature is ignored until the type is installed, at which point the function lazily redefines itself to use the additional signature. + +Pocomath now also allows template operations and template types, also built on top of typed-function (but candidates for integration therein). This is used to make many operations more specific, implement a type-homogeneous Tuple type, and make Complex numbers be type-homogeneous (which it seems like it always should be). One of the cutest consequences of this approach is that with careful definitions of the `Complex` templates, one gets a working quaternion data type absolutely for free as `Complex>` (and integral quaternions as `Complex>`, etc.) \ No newline at end of file From bcbb24acd258477ed6145716a510c7018fd02262 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sun, 7 Aug 2022 09:19:27 -0700 Subject: [PATCH 19/22] feat: Generic numerical types Inspired by https://github.com/josdejong/mathjs/discussions/2212 and https://github.com/josdejong/mathjs/issues/2585. Provides a simple adapter function `adapted` which takes a class implementing an arithmetical datatype and returns a PocomathInstance with a new type for that class, invoking the methods of the class in a standard way for the Pocomath/mathjs operations. (That instance can then be installed in another to add the new type to any instance you like, including the default one.) Uses this facility to bring fraction.js Fraction into Pocomath, and tests the resulting type. Currently the "standard" interface for an arithmetical type is heavily modeled after the design of fraction.js, but with experience with other 3rd-party types it could be streamlined to something pretty generic (and the Fraction adaptation could be patched to conform to the resulting "standard"). Or a proposal along the lines of https://github.com/josdejong/mathjs/discussions/2212 could be adopted, and a shim could be added to fraction.js to conform to **that** standard. Resolves #30. --- package.json5 | 1 + pnpm-lock.yaml | 6 +++ src/core/PocomathInstance.mjs | 10 +++- src/generic/Types/adapted.mjs | 88 ++++++++++++++++++++++++++++++++ src/generic/abs.mjs | 7 +++ src/generic/absquare.mjs | 6 +++ src/generic/all.mjs | 20 ++++++++ src/generic/arithmetic.mjs | 6 ++- src/generic/quotient.mjs | 3 ++ src/generic/relational.mjs | 23 +++++---- src/generic/roundquotient.mjs | 3 ++ src/number/isZero.mjs | 5 +- src/number/quotient.mjs | 17 +++++-- src/ops/floor.mjs | 7 ++- test/generic/fraction.mjs | 95 +++++++++++++++++++++++++++++++++++ 15 files changed, 278 insertions(+), 19 deletions(-) create mode 100644 src/generic/Types/adapted.mjs create mode 100644 src/generic/abs.mjs create mode 100644 src/generic/absquare.mjs create mode 100644 src/generic/quotient.mjs create mode 100644 src/generic/roundquotient.mjs create mode 100644 test/generic/fraction.mjs diff --git a/package.json5 b/package.json5 index a0647ae..1862178 100644 --- a/package.json5 +++ b/package.json5 @@ -24,6 +24,7 @@ }, dependencies: { 'bigint-isqrt': '^0.2.1', + 'fraction.js': '^4.2.0', 'typed-function': '^3.0.0', }, } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4063068..eef07ec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2,11 +2,13 @@ lockfileVersion: 5.4 specifiers: bigint-isqrt: ^0.2.1 + fraction.js: ^4.2.0 mocha: ^10.0.0 typed-function: ^3.0.0 dependencies: bigint-isqrt: 0.2.1 + fraction.js: 4.2.0 typed-function: 3.0.0 devDependencies: @@ -192,6 +194,10 @@ packages: hasBin: true dev: true + /fraction.js/4.2.0: + resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==} + dev: false + /fs.realpath/1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} dev: true diff --git a/src/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index bb013d1..1add7eb 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -263,6 +263,7 @@ export default class PocomathInstance { * @param {{test: any => bool, // the predicate for the type * from: Record => > // conversions * before: string[] // lower priority types + * refines: string // The type this is a subtype of * }} specification * * The second parameter of this function specifies the structure of the @@ -711,7 +712,12 @@ export default class PocomathInstance { if (argType === 'any') { throw TypeError( `In call to ${name}, incompatible template arguments: ` - + args.map(a => JSON.stringify(a)).join(', ')) + // + args.map(a => JSON.stringify(a)).join(', ') + // unfortunately barfs on bigints. Need a better formatter + // wish we could just use the one that console.log uses; + // is that accessible somehow? + + args.map(a => a.toString()).join(', ') + + ' of types ' + argTypes.join(', ') + argType) } argTypes.push(argType) } @@ -947,7 +953,7 @@ export default class PocomathInstance { } else { throw new Error( 'Implement inexact self-reference in typed-function for ' - + neededSig) + + `${name}(${neededSig})`) } } const refs = imps[aSignature].builtRefs diff --git a/src/generic/Types/adapted.mjs b/src/generic/Types/adapted.mjs new file mode 100644 index 0000000..ba20889 --- /dev/null +++ b/src/generic/Types/adapted.mjs @@ -0,0 +1,88 @@ +import PocomathInstance from '../../core/PocomathInstance.mjs' +/* creates a PocomathInstance incorporating a new numeric type encapsulated + * as a class. (This instance can the be `install()ed` in another to add the + * type so created.) + * + * @param {string} name The name of the new type + * @param {class} Thing The class implementing the new type + * @param {object} overrides Patches to the auto-generated adaptation + */ +export default function adapted(name, Thing, overrides) { + const thing = new PocomathInstance('Adapted Thing') + const test = overrides.isa || Thing.isa || (x => x instanceof Thing) + thing.installType(name, { + test, + from: overrides.from || {}, + before: overrides.before || [], + refines: overrides.refines || undefined + }) + + // Build the operations for Thing + const operations = {} + // first a creator function, with name depending on the name of the thing: + const creatorName = overrides.creatorName || name.toLowerCase() + const creator = overrides[creatorName] + ? overrides[creatorName]('') + : Thing[creatorName] + ? (Thing[creatorName]) + : ((...args) => new Thing(...args)) + const defaultCreatorImps = { + '': () => () => creator(), + '...any': () => args => creator(...args) + } + defaultCreatorImps[name] = () => x => x // x.clone(x)? + operations[creatorName] = overrides[creatorName] || defaultCreatorImps + + // We make the default instance, just as a place to check for methods + const instance = overrides.instance || creator() + + // Now adapt the methods to typed-function: + const unaryOps = { + abs: 'abs', + ceiling: 'ceil', + floor: 'floor', + invert: 'inverse', + round: 'round', + sqrt: 'sqrt', + negate: 'neg' + } + const binaryOps = { + add: 'add', + compare: 'compare', + divide: 'div', + equalTT: 'equals', + gcd: 'gcd', + lcm: 'lcm', + mod: 'mod', + multiply: 'mul', + subtract: 'sub' + } + for (const [mathname, standardname] of Object.entries(unaryOps)) { + if (standardname in instance) { + operations[mathname] = {} + operations[mathname][name] = () => t => t[standardname]() + } + } + operations.zero = {} + operations.zero[name] = () => t => creator() + operations.one = {} + operations.one[name] = () => t => creator(1) + operations.conjugate = {} + operations.conjugate[name] = () => t => t // or t.clone() ?? + + const binarySignature = `${name},${name}` + for (const [mathname, standardname] of Object.entries(binaryOps)) { + if (standardname in instance) { + operations[mathname] = {} + operations[mathname][binarySignature] = () => (t,u) => t[standardname](u) + } + } + if ('operations' in overrides) { + Object.assign(operations, overrides.operations) + } + + thing.install(operations) + return thing +} + +export {adapted} diff --git a/src/generic/abs.mjs b/src/generic/abs.mjs new file mode 100644 index 0000000..84ebc31 --- /dev/null +++ b/src/generic/abs.mjs @@ -0,0 +1,7 @@ +export const abs = { + T: ({ + 'smaller(T,T)': lt, + 'negate(T)': neg, + 'zero(T)': zr + }) => t => (smaller(t, zr(t)) ? neg(t) : t) +} diff --git a/src/generic/absquare.mjs b/src/generic/absquare.mjs new file mode 100644 index 0000000..26d6717 --- /dev/null +++ b/src/generic/absquare.mjs @@ -0,0 +1,6 @@ +export const absquare = { + T: ({ + 'square(T)': sq, + 'abs(T)': abval + }) => t => sq(abval(t)) +} diff --git a/src/generic/all.mjs b/src/generic/all.mjs index 19ba165..7132944 100644 --- a/src/generic/all.mjs +++ b/src/generic/all.mjs @@ -1,2 +1,22 @@ +import {adapted} from './Types/adapted.mjs' +import Fraction from 'fraction.js/bigfraction.js' + export * from './arithmetic.mjs' export * from './relational.mjs' + +export const fraction = adapted('Fraction', Fraction, { + before: ['Complex'], + from: {number: n => new Fraction(n)}, + operations: { + compare: {'Fraction,Fraction': () => (f,g) => new Fraction(f.compare(g))}, + mod: { + 'Fraction,Fraction': () => (n,d) => { + // patch for "mathematician's modulus" + // OK to use full public API of Fraction here + const fmod = n.mod(d) + if (fmod.s === -1n) return fmod.add(d.abs()) + return fmod + } + } + } +}) diff --git a/src/generic/arithmetic.mjs b/src/generic/arithmetic.mjs index 00faddb..51f0da9 100644 --- a/src/generic/arithmetic.mjs +++ b/src/generic/arithmetic.mjs @@ -2,14 +2,18 @@ import {reducingOperation} from './reducingOperation.mjs' export * from './Types/generic.mjs' +export {abs} from './abs.mjs' +export {absquare} from './absquare.mjs' export const add = reducingOperation +export {divide} from './divide.mjs' export const gcd = reducingOperation export {identity} from './identity.mjs' export {lcm} from './lcm.mjs' export {mean} from './mean.mjs' export {mod} from './mod.mjs' export const multiply = reducingOperation -export {divide} from './divide.mjs' +export {quotient} from './quotient.mjs' +export {roundquotient} from './roundquotient.mjs' export {sign} from './sign.mjs' export {sqrt} from './sqrt.mjs' export {square} from './square.mjs' diff --git a/src/generic/quotient.mjs b/src/generic/quotient.mjs new file mode 100644 index 0000000..54e000a --- /dev/null +++ b/src/generic/quotient.mjs @@ -0,0 +1,3 @@ +export const quotient = { + 'T,T': ({'floor(T)': flr, 'divide(T,T)':div}) => (n,d) => flr(div(n,d)) +} diff --git a/src/generic/relational.mjs b/src/generic/relational.mjs index 939ae19..a1639a1 100644 --- a/src/generic/relational.mjs +++ b/src/generic/relational.mjs @@ -3,7 +3,8 @@ export const compare = { } export const isZero = { - 'undefined': () => u => u === 0 + 'undefined': () => u => u === 0, + T: ({'equal(T,T)': eq, 'zero(T)': zr}) => t => eq(t, zr(t)) } export const equal = { @@ -33,18 +34,20 @@ export const unequal = { export const larger = { 'T,T': ({ 'compare(T,T)': cmp, - 'one(T)' : uno - }) => (x,y) => cmp(x,y) === uno(y) + 'one(T)' : uno, + 'equalTT(T,T)' : eq + }) => (x,y) => eq(cmp(x,y), uno(y)) } export const largerEq = { 'T,T': ({ 'compare(T,T)': cmp, 'one(T)' : uno, - 'isZero(T)' : isZ + 'isZero(T)' : isZ, + 'equalTT(T,T)': eq }) => (x,y) => { const c = cmp(x,y) - return isZ(c) || c === uno(y) + return isZ(c) || eq(c, uno(y)) } } @@ -52,16 +55,18 @@ export const smaller = { 'T,T': ({ 'compare(T,T)': cmp, 'one(T)' : uno, - 'isZero(T)' : isZ + 'isZero(T)' : isZ, + unequal }) => (x,y) => { const c = cmp(x,y) - return !isZ(c) && c !== uno(y) + return !isZ(c) && unequal(c, uno(y)) } } export const smallerEq = { 'T,T': ({ 'compare(T,T)': cmp, - 'one(T)' : uno - }) => (x,y) => cmp(x,y) !== uno(y) + 'one(T)' : uno, + unequal + }) => (x,y) => unequal(cmp(x,y), uno(y)) } diff --git a/src/generic/roundquotient.mjs b/src/generic/roundquotient.mjs new file mode 100644 index 0000000..5346882 --- /dev/null +++ b/src/generic/roundquotient.mjs @@ -0,0 +1,3 @@ +export const roundquotient = { + 'T,T': ({'round(T)': rnd, 'divide(T,T)':div}) => (n,d) => rnd(div(n,d)) +} diff --git a/src/number/isZero.mjs b/src/number/isZero.mjs index ac98ce2..c15549e 100644 --- a/src/number/isZero.mjs +++ b/src/number/isZero.mjs @@ -1,3 +1,6 @@ export * from './Types/number.mjs' -export const isZero = {number: () => n => n === 0} +export const isZero = { + number: () => n => n === 0, + NumInt: () => n => n === 0 // necessary because of generic template +} diff --git a/src/number/quotient.mjs b/src/number/quotient.mjs index d86b832..e8ed83a 100644 --- a/src/number/quotient.mjs +++ b/src/number/quotient.mjs @@ -1,8 +1,15 @@ export * from './Types/number.mjs' -export const quotient = { - 'number,number': () => (n,d) => { - if (d === 0) return d - return Math.floor(n/d) - } +const intquotient = () => (n,d) => { + if (d === 0) return d + return Math.floor(n/d) +} + +export const quotient = { + // Hmmm, seem to need all of these because of the generic template version + // Should be a way around that + 'NumInt,NumInt': intquotient, + 'NumInt,number': intquotient, + 'number,NumInt': intquotient, + 'number,number': intquotient } diff --git a/src/ops/floor.mjs b/src/ops/floor.mjs index 71c8e70..5fdb9e7 100644 --- a/src/ops/floor.mjs +++ b/src/ops/floor.mjs @@ -21,5 +21,10 @@ export const floor = { // OK to include a type totally not in Pocomath yet, it'll never be // activated. - Fraction: ({quotient}) => f => quotient(f.n, f.d), + // Fraction: ({quotient}) => f => quotient(f.n, f.d), // oops have that now + BigNumber: ({ + 'round(BigNumber)': rnd, + 'equal(BigNumber,BigNumber)': eq + }) => x => eq(x,round(x)) ? round(x) : x.floor() + } diff --git a/test/generic/fraction.mjs b/test/generic/fraction.mjs new file mode 100644 index 0000000..9af3b8d --- /dev/null +++ b/test/generic/fraction.mjs @@ -0,0 +1,95 @@ +import assert from 'assert' +import math from '../../src/pocomath.mjs' +import Fraction from 'fraction.js/bigfraction.js' + +describe('fraction', () => { + const half = new Fraction(1/2) + const tf = new Fraction(3, 4) + let zero // will fill in during a test + const one = new Fraction(1) + + it('supports typeOf', () => { + assert.strictEqual(math.typeOf(half), 'Fraction') + }) + + it('can be built', () => { + zero = math.fraction() + assert.deepStrictEqual(zero, new Fraction(0)) + assert.deepStrictEqual(math.fraction(1/2), half) + assert.strictEqual(math.fraction(half), half) // maybe it should be a clone? + assert.strictEqual(math.fraction(9, 16).valueOf(), 9/16) + assert.strictEqual(math.fraction(9n, 16n).valueOf(), 9/16) + }) + + it('has abs and sign', () => { + assert.deepStrictEqual(math.abs(math.fraction('-1/2')), half) + assert.deepStrictEqual(math.sign(math.negate(tf)), math.negate(one)) + }) + + it('can add and multiply', () => { + assert.strictEqual(math.add(half, 1).valueOf(), 1.5) + assert.strictEqual(math.multiply(2, half).valueOf(), 1) + }) + + it('can subtract and divide', () => { + assert.strictEqual(math.subtract(half,tf).valueOf(), -0.25) + assert.strictEqual(math.divide(tf,half).valueOf(), 1.5) + }) + + it('computes mod', () => { + assert.strictEqual(math.mod(tf, half).valueOf(), 0.25) + assert.strictEqual(math.mod(tf, math.negate(half)).valueOf(), 0.25) + assert.strictEqual(math.mod(math.negate(tf), half).valueOf(), 0.25) + assert.strictEqual( + math.mod(math.negate(tf), math.negate(half)).valueOf(), + 0.25) + assert.deepStrictEqual( + math.mod(math.fraction(-1, 3), half), + math.fraction(1, 6)) + }) + + it('supports conjugate', () => { + assert.strictEqual(math.conjugate(half), half) + }) + + it('can compare fractions', () => { + assert.deepStrictEqual(math.compare(tf, half), one) + assert.strictEqual(math.equal(half, math.fraction("2/4")), true) + assert.strictEqual(math.smaller(half, tf), true) + assert.strictEqual(math.larger(half, tf), false) + assert.strictEqual(math.smallerEq(tf, math.fraction(0.75)), true) + assert.strictEqual(math.largerEq(tf, half), true) + assert.strictEqual(math.unequal(half, tf), true) + assert.strictEqual(math.isZero(math.zero(tf)), true) + assert.strictEqual(math.isZero(half), false) + }) + + it('computes gcd and lcm', () => { + assert.strictEqual(math.gcd(half,tf).valueOf(), 0.25) + assert.strictEqual(math.lcm(half,tf).valueOf(), 1.5) + }) + + it('computes additive and multiplicative inverses', () => { + assert.strictEqual(math.negate(half).valueOf(), -0.5) + assert.deepStrictEqual(math.invert(tf), math.fraction('4/3')) + }) + + it('computes integer parts and quotients', () => { + assert.deepStrictEqual(math.floor(tf), zero) + assert.deepStrictEqual(math.round(tf), one) + assert.deepStrictEqual(math.ceiling(half), one) + assert.deepStrictEqual(math.quotient(tf, half), one) + assert.deepStrictEqual( + math.roundquotient(math.fraction(7/8), half), + math.multiply(2,math.one(tf))) + }) + + it('has no sqrt (although that should be patched)', () => { + assert.throws(() => math.sqrt(math.fraction(9/16)), TypeError) + }) + + it('but it can square', () => { + assert.deepStrictEqual(math.square(tf), math.fraction(9/16)) + }) + +}) From 207ac4330b0338e0a54fb3444bd0936daa9ebe63 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sun, 7 Aug 2022 16:38:15 +0000 Subject: [PATCH 20/22] doc(README): Add a note about adopting 3rd party types --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ea194c1..75a378e 100644 --- a/README.md +++ b/README.md @@ -26,4 +26,6 @@ Note that Pocomath allows one implementation to depend just on a specific signat Pocomath also lazily reloads operations that depend on the config when that changes, and if an operation has a signature mentioning an undefined type, that signature is ignored until the type is installed, at which point the function lazily redefines itself to use the additional signature. -Pocomath now also allows template operations and template types, also built on top of typed-function (but candidates for integration therein). This is used to make many operations more specific, implement a type-homogeneous Tuple type, and make Complex numbers be type-homogeneous (which it seems like it always should be). One of the cutest consequences of this approach is that with careful definitions of the `Complex` templates, one gets a working quaternion data type absolutely for free as `Complex>` (and integral quaternions as `Complex>`, etc.) \ No newline at end of file +Pocomath now also allows template operations and template types, also built on top of typed-function (but candidates for integration therein). This is used to make many operations more specific, implement a type-homogeneous Tuple type, and make Complex numbers be type-homogeneous (which it seems like it always should be). One of the cutest consequences of this approach is that with careful definitions of the `Complex` templates, one gets a working quaternion data type absolutely for free as `Complex>` (and integral quaternions as `Complex>`, etc.) + +It also now has a facility to adapt a third-party numeric class as a type in Pocomath, see `src/generic/all.mjs` and `src/generic/Types/adapted.mjs`, which it uses by way of example to incorporate fraction.js Fraction objects into Pocomath. \ No newline at end of file From 31add66f4cc1f5768c8e0697214dbcd624754bf0 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Tue, 30 Aug 2022 19:36:44 +0000 Subject: [PATCH 21/22] feat: Return type annotations (#53) Provides the infrastructure to allow annotating the return types of functions, and does so for essentially every operation in the system (the only known exceptions being add, multiply, etc., on arbitrarily many arguments). One main infrastructure enhancements are bounded template types, e.g. `T:number` being a template parameter where T can take on the type `number` or any subtype thereof. A main internal enhancement is that base template types are no longer added to the typed universe; rather, there is a secondary, "meta" typed universe where they live. The primary point/purpose of this change is then the necessary search order for implementations can be much better modeled by typed-function's search order, using the `onMismatch` facility to redirect the search from fully instantiated implementations to the generic catchall implementations for each template (these catchalls live in the meta universe). Numerous other small improvements and bugfixes were encountered along the way. Co-authored-by: Glen Whitney Reviewed-on: https://code.studioinfinity.org/glen/pocomath/pulls/53 --- src/bigint/absquare.mjs | 3 +- src/bigint/add.mjs | 3 +- src/bigint/compare.mjs | 4 +- src/bigint/divide.mjs | 7 +- src/bigint/isZero.mjs | 3 +- src/bigint/multiply.mjs | 3 +- src/bigint/native.mjs | 4 +- src/bigint/negate.mjs | 3 +- src/bigint/one.mjs | 3 +- src/bigint/quotient.mjs | 7 +- src/bigint/roundquotient.mjs | 5 +- src/bigint/sign.mjs | 5 +- src/bigint/sqrt.mjs | 13 +- src/bigint/zero.mjs | 3 +- src/complex/Types/Complex.mjs | 20 +- src/complex/abs.mjs | 19 +- src/complex/absquare.mjs | 24 +- src/complex/add.mjs | 4 +- src/complex/associate.mjs | 5 +- src/complex/complex.mjs | 14 +- src/complex/conjugate.mjs | 4 +- src/complex/equalTT.mjs | 12 +- src/complex/gcd.mjs | 8 +- src/complex/invert.mjs | 6 +- src/complex/isZero.mjs | 4 +- src/complex/multiply.mjs | 17 +- src/complex/quaternion.mjs | 11 +- src/complex/quotient.mjs | 4 +- src/complex/roundquotient.mjs | 6 +- src/complex/sqrt.mjs | 43 +- src/core/PocomathInstance.mjs | 1229 +++++++++++++++++++++-------- src/core/Returns.mjs | 34 + src/core/utils.mjs | 2 + src/generic/Types/adapted.mjs | 43 +- src/generic/abs.mjs | 4 +- src/generic/absquare.mjs | 5 +- src/generic/all.mjs | 10 +- src/generic/divide.mjs | 5 +- src/generic/gcdType.mjs | 6 +- src/generic/identity.mjs | 12 +- src/generic/lcm.mjs | 4 +- src/generic/mean.mjs | 7 +- src/generic/mod.mjs | 5 +- src/generic/quotient.mjs | 8 +- src/generic/reducingOperation.mjs | 15 +- src/generic/relational.mjs | 37 +- src/generic/roundquotient.mjs | 8 +- src/generic/sign.mjs | 8 +- src/generic/sqrt.mjs | 3 +- src/generic/square.mjs | 5 +- src/generic/subtract.mjs | 8 +- src/number/abs.mjs | 3 +- src/number/absquare.mjs | 3 +- src/number/add.mjs | 7 +- src/number/compare.mjs | 5 +- src/number/invert.mjs | 4 +- src/number/isZero.mjs | 4 +- src/number/multiply.mjs | 4 +- src/number/native.mjs | 4 +- src/number/negate.mjs | 5 +- src/number/one.mjs | 4 +- src/number/quotient.mjs | 17 +- src/number/roundquotient.mjs | 6 +- src/number/sqrt.mjs | 17 +- src/number/zero.mjs | 3 +- src/ops/choose.mjs | 10 +- src/ops/factorial.mjs | 11 +- src/ops/floor.mjs | 23 +- src/tuple/Types/Tuple.mjs | 111 +-- src/tuple/equalTT.mjs | 9 +- src/tuple/isZero.mjs | 5 +- src/tuple/length.mjs | 3 +- src/tuple/tuple.mjs | 6 +- test/_pocomath.mjs | 49 +- test/complex/_all.mjs | 5 +- test/core/_utils.mjs | 8 + test/custom.mjs | 9 +- test/generic/_all.mjs | 20 + test/generic/fraction.mjs | 6 + test/tuple/_native.mjs | 22 +- 80 files changed, 1502 insertions(+), 606 deletions(-) create mode 100644 src/core/Returns.mjs create mode 100644 test/core/_utils.mjs create mode 100644 test/generic/_all.mjs diff --git a/src/bigint/absquare.mjs b/src/bigint/absquare.mjs index 4c2040a..587e8ec 100644 --- a/src/bigint/absquare.mjs +++ b/src/bigint/absquare.mjs @@ -1,6 +1,7 @@ +import Returns from '../core/Returns.mjs' export * from './Types/bigint.mjs' /* Absolute value squared */ export const absquare = { - bigint: ({'square(bigint)': sqb}) => b => sqb(b) + bigint: ({'square(bigint)': sqb}) => Returns('bigint', b => sqb(b)) } diff --git a/src/bigint/add.mjs b/src/bigint/add.mjs index 1cd296d..c4291ac 100644 --- a/src/bigint/add.mjs +++ b/src/bigint/add.mjs @@ -1,3 +1,4 @@ +import Returns from '../core/Returns.mjs' export * from './Types/bigint.mjs' -export const add = {'bigint,bigint': () => (a,b) => a+b} +export const add = {'bigint,bigint': () => Returns('bigint', (a,b) => a+b)} diff --git a/src/bigint/compare.mjs b/src/bigint/compare.mjs index ab830ab..097dfca 100644 --- a/src/bigint/compare.mjs +++ b/src/bigint/compare.mjs @@ -1,5 +1,7 @@ +import Returns from '../core/Returns.mjs' export * from './Types/bigint.mjs' export const compare = { - 'bigint,bigint': () => (a,b) => a === b ? 0n : (a > b ? 1n : -1n) + 'bigint,bigint': () => Returns( + 'boolean', (a,b) => a === b ? 0n : (a > b ? 1n : -1n)) } diff --git a/src/bigint/divide.mjs b/src/bigint/divide.mjs index 4554457..492893a 100644 --- a/src/bigint/divide.mjs +++ b/src/bigint/divide.mjs @@ -1,12 +1,13 @@ +import Returns from '../core/Returns.mjs' export * from './Types/bigint.mjs' export const divide = { 'bigint,bigint': ({config, 'quotient(bigint,bigint)': quot}) => { - if (config.predictable) return quot - return (n, d) => { + if (config.predictable) return Returns('bigint', (n,d) => quot(n,d)) + return Returns('bigint|undefined', (n, d) => { const q = n/d if (q * d == n) return q return undefined - } + }) } } diff --git a/src/bigint/isZero.mjs b/src/bigint/isZero.mjs index 0efa71c..02aca57 100644 --- a/src/bigint/isZero.mjs +++ b/src/bigint/isZero.mjs @@ -1,3 +1,4 @@ +import Returns from '../core/Returns.mjs' export * from './Types/bigint.mjs' -export const isZero = {bigint: () => b => b === 0n} +export const isZero = {bigint: () => Returns('boolean', b => b === 0n)} diff --git a/src/bigint/multiply.mjs b/src/bigint/multiply.mjs index e19959e..e80cee0 100644 --- a/src/bigint/multiply.mjs +++ b/src/bigint/multiply.mjs @@ -1,3 +1,4 @@ +import Returns from '../core/Returns.mjs' export * from './Types/bigint.mjs' -export const multiply = {'bigint,bigint': () => (a,b) => a*b} +export const multiply = {'bigint,bigint': () => Returns('bigint', (a,b) => a*b)} diff --git a/src/bigint/native.mjs b/src/bigint/native.mjs index 6cc76d4..b387615 100644 --- a/src/bigint/native.mjs +++ b/src/bigint/native.mjs @@ -1,12 +1,12 @@ import gcdType from '../generic/gcdType.mjs' -import {identity} from '../generic/identity.mjs' +import {identityType} from '../generic/identity.mjs' export * from './Types/bigint.mjs' export {absquare} from './absquare.mjs' export {add} from './add.mjs' export {compare} from './compare.mjs' -export const conjugate = {bigint: () => identity} +export const conjugate = {bigint: identityType('bigint')} export {divide} from './divide.mjs' export const gcd = gcdType('bigint') export {isZero} from './isZero.mjs' diff --git a/src/bigint/negate.mjs b/src/bigint/negate.mjs index d44cdb0..ecd51f1 100644 --- a/src/bigint/negate.mjs +++ b/src/bigint/negate.mjs @@ -1,3 +1,4 @@ +import Returns from '../core/Returns.mjs' export * from './Types/bigint.mjs' -export const negate = {bigint: () => b => -b} +export const negate = {bigint: () => Returns('bigint', b => -b)} diff --git a/src/bigint/one.mjs b/src/bigint/one.mjs index f548a65..8e8a7f2 100644 --- a/src/bigint/one.mjs +++ b/src/bigint/one.mjs @@ -1,3 +1,4 @@ +import Returns from '../core/Returns.mjs' export * from './Types/bigint.mjs' -export const one = {bigint: () => () => 1n} +export const one = {bigint: () => Returns('bigint', () => 1n)} diff --git a/src/bigint/quotient.mjs b/src/bigint/quotient.mjs index 589adc3..c1a086a 100644 --- a/src/bigint/quotient.mjs +++ b/src/bigint/quotient.mjs @@ -1,13 +1,14 @@ +import Returns from '../core/Returns.mjs' export * from './Types/bigint.mjs' -/* Returns the best integer approximation to n/d */ +/* Returns the floor integer approximation to n/d */ export const quotient = { - 'bigint,bigint': ({'sign(bigint)': sgn}) => (n, d) => { + 'bigint,bigint': ({'sign(bigint)': sgn}) => Returns('bigint', (n, d) => { const dSgn = sgn(d) if (dSgn === 0n) return 0n if (sgn(n) === dSgn) return n/d const quot = n/d if (quot * d == n) return quot return quot - 1n - } + }) } diff --git a/src/bigint/roundquotient.mjs b/src/bigint/roundquotient.mjs index c6763a2..57fb941 100644 --- a/src/bigint/roundquotient.mjs +++ b/src/bigint/roundquotient.mjs @@ -1,8 +1,9 @@ +import Returns from '../core/Returns.mjs' export * from './Types/bigint.mjs' /* Returns the closest integer approximation to n/d */ export const roundquotient = { - 'bigint,bigint': ({'sign(bigint)': sgn}) => (n, d) => { + 'bigint,bigint': ({'sign(bigint)': sgn}) => Returns('bigint', (n, d) => { const dSgn = sgn(d) if (dSgn === 0n) return 0n const candidate = n/d @@ -11,5 +12,5 @@ export const roundquotient = { if (2n * rem > absd) return candidate + dSgn if (-2n * rem >= absd) return candidate - dSgn return candidate - } + }) } diff --git a/src/bigint/sign.mjs b/src/bigint/sign.mjs index af48e05..c811df2 100644 --- a/src/bigint/sign.mjs +++ b/src/bigint/sign.mjs @@ -1,9 +1,10 @@ +import Returns from '../core/Returns.mjs' export * from './Types/bigint.mjs' export const sign = { - bigint: () => b => { + bigint: () => Returns('bigint', b => { if (b === 0n) return 0n if (b > 0n) return 1n return -1n - } + }) } diff --git a/src/bigint/sqrt.mjs b/src/bigint/sqrt.mjs index 4d34513..01ef0b0 100644 --- a/src/bigint/sqrt.mjs +++ b/src/bigint/sqrt.mjs @@ -1,5 +1,6 @@ -export * from './Types/bigint.mjs' +import Returns from '../core/Returns.mjs' import isqrt from 'bigint-isqrt' +export * from './Types/bigint.mjs' export const sqrt = { bigint: ({ @@ -11,18 +12,18 @@ export const sqrt = { // Don't just return the constant isqrt here because the object // gets decorated with info that might need to be different // for different PocomathInstancss - return b => isqrt(b) + return Returns('bigint', b => isqrt(b)) } if (!cplx) { - return b => { + return Returns('bigint|undefined', b => { if (b >= 0n) { const trial = isqrt(b) if (trial * trial === b) return trial } return undefined - } + }) } - return b => { + return Returns('bigint|Complex|undefined', b => { if (b === undefined) return undefined let real = true if (b < 0n) { @@ -33,6 +34,6 @@ export const sqrt = { if (trial * trial !== b) return undefined if (real) return trial return cplx(0n, trial) - } + }) } } diff --git a/src/bigint/zero.mjs b/src/bigint/zero.mjs index 0c63a1a..e9fe83b 100644 --- a/src/bigint/zero.mjs +++ b/src/bigint/zero.mjs @@ -1,3 +1,4 @@ +import Returns from '../core/Returns.mjs' export * from './Types/bigint.mjs' -export const zero = {bigint: () => () => 0n} +export const zero = {bigint: () => Returns('bigint', () => 0n)} diff --git a/src/complex/Types/Complex.mjs b/src/complex/Types/Complex.mjs index 0fa4107..4d63417 100644 --- a/src/complex/Types/Complex.mjs +++ b/src/complex/Types/Complex.mjs @@ -1,15 +1,14 @@ +import {Returns, returnTypeOf} from '../../core/Returns.mjs' import PocomathInstance from '../../core/PocomathInstance.mjs' const Complex = new PocomathInstance('Complex') -// Base type that should generally not be used directly -Complex.installType('Complex', { - test: z => z && typeof z === 'object' && 're' in z && 'im' in z -}) -// Now the template type: Complex numbers are actually always homeogeneous -// in their component types. +// Now the template type: Complex numbers are actually always homogeneous +// in their component types. For an explanation of the meanings of the +// properties, see ../../tuple/Types/Tuple.mjs Complex.installType('Complex', { - infer: ({typeOf, joinTypes}) => z => joinTypes([typeOf(z.re), typeOf(z.im)]), + base: z => z && typeof z === 'object' && 're' in z && 'im' in z, test: testT => z => testT(z.re) && testT(z.im), + infer: ({typeOf, joinTypes}) => z => joinTypes([typeOf(z.re), typeOf(z.im)]), from: { T: t => ({re: t, im: t-t}), // hack: maybe need a way to call zero(T) U: convert => u => { @@ -21,7 +20,12 @@ Complex.installType('Complex', { }) Complex.promoteUnary = { - 'Complex': ({'self(T)': me, complex}) => z => complex(me(z.re), me(z.im)) + 'Complex': ({ + T, + 'self(T)': me, + complex + }) => Returns( + `Complex<${returnTypeOf(me)}>`, z => complex(me(z.re), me(z.im))) } export {Complex} diff --git a/src/complex/abs.mjs b/src/complex/abs.mjs index 536b8b4..53a2464 100644 --- a/src/complex/abs.mjs +++ b/src/complex/abs.mjs @@ -1,10 +1,21 @@ +import {Returns, returnTypeOf} from '../core/Returns.mjs' export * from './Types/Complex.mjs' export const abs = { 'Complex': ({ - sqrt, // Calculation of the type needed in the square root (the - // underlying numeric type of T, whatever T is, is beyond Pocomath's - // (current) template abilities, so punt and just do full resolution + T, + sqrt, // Unfortunately no notation yet for the needed signature + 'absquare(T)': baseabsq, 'absquare(Complex)': absq - }) => z => sqrt(absq(z)) + }) => { + const midType = returnTypeOf(baseabsq) + const sqrtImp = sqrt.fromInstance.resolve('sqrt', midType, sqrt) + let retType = returnTypeOf(sqrtImp) + if (retType.includes('|')) { + // This is a bit of a hack, as it relies on all implementations of + // sqrt returning the "typical" return type as the first option + retType = retType.split('|',1)[0] + } + return Returns(retType, z => sqrtImp(absq(z))) + } } diff --git a/src/complex/absquare.mjs b/src/complex/absquare.mjs index bb4677f..ab0194c 100644 --- a/src/complex/absquare.mjs +++ b/src/complex/absquare.mjs @@ -1,9 +1,27 @@ +import {Returns, returnTypeOf} from '../core/Returns.mjs' export * from './Types/Complex.mjs' export const absquare = { 'Complex': ({ - add, // Calculation of exact type needed in add (underlying numeric of T) - // is (currently) too involved for Pocomath + add, // no easy way to write the needed signature; if T is number + // it is number,number; but if T is Complex, it is just + // bigint,bigint. So unfortunately we depend on all of add, and + // we extract the needed implementation below. 'self(T)': absq - }) => z => add(absq(z.re), absq(z.im)) + }) => { + const midType = returnTypeOf(absq) + const addImp = add.fromInstance.resolve( + 'add', `${midType},${midType}`, add) + return Returns( + returnTypeOf(addImp), z => addImp(absq(z.re), absq(z.im))) + } } + +/* We could imagine notations that Pocomath could support that would simplify + * the above, maybe something like + * 'Complex': ({ + * 'self(T): U': absq, + * 'add(U,U):V': plus, + * V + * }) => Returns(V, z => plus(absq(z.re), absq(z.im))) + */ diff --git a/src/complex/add.mjs b/src/complex/add.mjs index 9afdd90..6172326 100644 --- a/src/complex/add.mjs +++ b/src/complex/add.mjs @@ -1,8 +1,10 @@ +import Returns from '../core/Returns.mjs' export * from './Types/Complex.mjs' export const add = { 'Complex,Complex': ({ + T, 'self(T,T)': me, 'complex(T,T)': cplx - }) => (w,z) => cplx(me(w.re, z.re), me(w.im, z.im)) + }) => Returns(`Complex<${T}>`, (w,z) => cplx(me(w.re, z.re), me(w.im, z.im))) } diff --git a/src/complex/associate.mjs b/src/complex/associate.mjs index 10c799c..9aae6e1 100644 --- a/src/complex/associate.mjs +++ b/src/complex/associate.mjs @@ -1,3 +1,4 @@ +import Returns from '../core/Returns.mjs' export * from './Types/Complex.mjs' /* Returns true if w is z multiplied by a complex unit */ @@ -9,9 +10,9 @@ export const associate = { 'one(T)': uno, 'complex(T,T)': cplx, 'negate(Complex)': neg - }) => (w,z) => { + }) => Returns('boolean', (w,z) => { if (eq(w,z) || eq(w,neg(z))) return true const ti = times(z, cplx(zr(z.re), uno(z.im))) return eq(w,ti) || eq(w,neg(ti)) - } + }) } diff --git a/src/complex/complex.mjs b/src/complex/complex.mjs index a5a24f5..49cfa60 100644 --- a/src/complex/complex.mjs +++ b/src/complex/complex.mjs @@ -1,3 +1,4 @@ +import Returns from '../core/Returns.mjs' export * from './Types/Complex.mjs' export * from '../generic/Types/generic.mjs' @@ -6,15 +7,16 @@ export const complex = { * have a numeric/scalar type, e.g. by implementing subtypes in * typed-function */ - 'undefined': () => u => u, - 'undefined,any': () => (u, y) => u, - 'any,undefined': () => (x, u) => u, - 'undefined,undefined': () => (u, v) => u, - 'T,T': () => (x, y) => ({re: x, im: y}), + 'undefined': () => Returns('undefined', u => u), + 'undefined,any': () => Returns('undefined', (u, y) => u), + 'any,undefined': () => Returns('undefined', (x, u) => u), + 'undefined,undefined': () => Returns('undefined', (u, v) => u), + 'T,T': ({T}) => Returns(`Complex<${T}>`, (x, y) => ({re: x, im: y})), /* Take advantage of conversions in typed-function */ // 'Complex': () => z => z /* But help out because without templates built in to typed-function, * type inference turns out to be too hard */ - 'T': ({'zero(T)': zr}) => x => ({re: x, im: zr(x)}) + 'T': ({T, 'zero(T)': zr}) => Returns( + `Complex<${T}>`, x => ({re: x, im: zr(x)})) } diff --git a/src/complex/conjugate.mjs b/src/complex/conjugate.mjs index b94495d..c81180e 100644 --- a/src/complex/conjugate.mjs +++ b/src/complex/conjugate.mjs @@ -1,9 +1,11 @@ +import Returns from '../core/Returns.mjs' export * from './Types/Complex.mjs' export const conjugate = { 'Complex': ({ + T, 'negate(T)': neg, 'complex(T,T)': cplx - }) => z => cplx(z.re, neg(z.im)) + }) => Returns(`Complex<${T}>`, z => cplx(z.re, neg(z.im))) } diff --git a/src/complex/equalTT.mjs b/src/complex/equalTT.mjs index 6899aa0..6a84c9a 100644 --- a/src/complex/equalTT.mjs +++ b/src/complex/equalTT.mjs @@ -1,9 +1,11 @@ +import Returns from '../core/Returns.mjs' export * from './Types/Complex.mjs' export const equalTT = { 'Complex,Complex': ({ + T, 'self(T,T)': me - }) => (w,z) => me(w.re, z.re) && me(w.im, z.im), + }) => Returns('boolean', (w, z) => me(w.re, z.re) && me(w.im, z.im)), // NOTE: Although I do not understand exactly why, with typed-function@3.0's // matching algorithm, the above template must come first to ensure the // most specific match to a template call. I.e, if one of the below @@ -11,16 +13,16 @@ export const equalTT = { // with (Complex>, Complex) (!, hopefully in some // future iteration typed-function will be smart enough to prefer // Complex, Complex. Possibly the problem is in Pocomath's bolted-on - // type resolution and the difficulty will go away when features are moved into - // typed-function. + // type resolution and the difficulty will go away when features are moved + // into typed-function. 'Complex,T': ({ 'isZero(T)': isZ, 'self(T,T)': eqReal - }) => (z, x) => eqReal(z.re, x) && isZ(z.im), + }) => Returns('boolean', (z, x) => eqReal(z.re, x) && isZ(z.im)), 'T,Complex': ({ 'isZero(T)': isZ, 'self(T,T)': eqReal - }) => (b, z) => eqReal(z.re, b) && isZ(z.im), + }) => Returns('boolean', (b, z) => eqReal(z.re, b) && isZ(z.im)), } diff --git a/src/complex/gcd.mjs b/src/complex/gcd.mjs index 30b7dad..be90e4a 100644 --- a/src/complex/gcd.mjs +++ b/src/complex/gcd.mjs @@ -1,5 +1,6 @@ import PocomathInstance from '../core/PocomathInstance.mjs' -import * as Complex from './Types/Complex.mjs' +import Returns from '../core/Returns.mjs' +import * as Complex from './Types/Complex.mjs' import gcdType from '../generic/gcdType.mjs' const gcdComplexRaw = {} @@ -9,15 +10,16 @@ const imps = { gcdComplexRaw, gcd: { // Only return gcds with positive real part 'Complex,Complex': ({ + T, 'gcdComplexRaw(Complex,Complex)': gcdRaw, 'sign(T)': sgn, 'one(T)': uno, 'negate(Complex)': neg - }) => (z,m) => { + }) => Returns(`Complex<${T}>`, (z,m) => { const raw = gcdRaw(z, m) if (sgn(raw.re) === uno(raw.re)) return raw return neg(raw) - } + }) } } diff --git a/src/complex/invert.mjs b/src/complex/invert.mjs index 2f68e43..ce1b932 100644 --- a/src/complex/invert.mjs +++ b/src/complex/invert.mjs @@ -1,14 +1,16 @@ +import Returns from '../core/Returns.mjs' export * from './Types/Complex.mjs' export const invert = { 'Complex': ({ + T, 'conjugate(Complex)': conj, 'absquare(Complex)': asq, 'complex(T,T)': cplx, 'divide(T,T)': div - }) => z => { + }) => Returns(`Complex<${T}>`, z => { const c = conj(z) const d = asq(z) return cplx(div(c.re, d), div(c.im, d)) - } + }) } diff --git a/src/complex/isZero.mjs b/src/complex/isZero.mjs index 01a2f51..3e10c9b 100644 --- a/src/complex/isZero.mjs +++ b/src/complex/isZero.mjs @@ -1,5 +1,7 @@ +import Returns from '../core/Returns.mjs' export * from './Types/Complex.mjs' export const isZero = { - 'Complex': ({'self(T)': me}) => z => me(z.re) && me(z.im) + 'Complex': ({'self(T)': me}) => Returns( + 'boolean', z => me(z.re) && me(z.im)) } diff --git a/src/complex/multiply.mjs b/src/complex/multiply.mjs index e059a91..5d9edc6 100644 --- a/src/complex/multiply.mjs +++ b/src/complex/multiply.mjs @@ -1,15 +1,20 @@ +import Returns from '../core/Returns.mjs' export * from './Types/Complex.mjs' export const multiply = { 'Complex,Complex': ({ + T, 'complex(T,T)': cplx, 'add(T,T)': plus, - 'subtract(T,T)': sub, + 'subtract(T,T)': subt, 'self(T,T)': me, 'conjugate(T)': conj // makes quaternion multiplication work - }) => (w,z) => { - return cplx( - sub(me(w.re, z.re), me(conj(w.im), z.im)), - plus(me(conj(w.re), z.im), me(w.im, z.re))) - } + }) => Returns( + `Complex<${T}>`, + (w,z) => { + const realpart = subt(me( w.re, z.re), me(conj(w.im), z.im)) + const imagpart = plus(me(conj(w.re), z.im), me( w.im, z.re)) + return cplx(realpart, imagpart) + } + ) } diff --git a/src/complex/quaternion.mjs b/src/complex/quaternion.mjs index 4f35d30..a435395 100644 --- a/src/complex/quaternion.mjs +++ b/src/complex/quaternion.mjs @@ -1,5 +1,14 @@ +import Returns from '../core/Returns.mjs' export * from './Types/Complex.mjs' +// Might be nice to have type aliases! export const quaternion = { - 'T,T,T,T': ({complex}) => (r,i,j,k) => complex(complex(r,j), complex(i,k)) + 'T,T,T,T': ({ + T, + 'complex(T,T)': cplxT, + 'complex(Complex,Complex)': quat + }) => Returns( + `Complex>`, + (r,i,j,k) => quat(cplxT(r,j), cplxT(i,k)) + ) } diff --git a/src/complex/quotient.mjs b/src/complex/quotient.mjs index 32299ca..6b53de1 100644 --- a/src/complex/quotient.mjs +++ b/src/complex/quotient.mjs @@ -1,7 +1,9 @@ +import Returns from '../core/Returns.mjs' export * from './roundquotient.mjs' export const quotient = { 'Complex,Complex': ({ + T, 'roundquotient(Complex,Complex)': rq - }) => (w,z) => rq(w,z) + }) => Returns(`Complex<${T}>`, (w,z) => rq(w,z)) } diff --git a/src/complex/roundquotient.mjs b/src/complex/roundquotient.mjs index 5c25765..474077f 100644 --- a/src/complex/roundquotient.mjs +++ b/src/complex/roundquotient.mjs @@ -1,17 +1,19 @@ +import Returns from '../core/Returns.mjs' export * from './Types/Complex.mjs' export const roundquotient = { 'Complex,Complex': ({ + T, 'isZero(Complex)': isZ, 'conjugate(Complex)': conj, 'multiply(Complex,Complex)': mult, 'absquare(Complex)': asq, 'self(T,T)': me, 'complex(T,T)': cplx - }) => (n,d) => { + }) => Returns(`Complex<${T}>`, (n,d) => { if (isZ(d)) return d const cnum = mult(n, conj(d)) const dreal = asq(d) return cplx(me(cnum.re, dreal), me(cnum.im, dreal)) - } + }) } diff --git a/src/complex/sqrt.mjs b/src/complex/sqrt.mjs index d60ed6a..7f4044b 100644 --- a/src/complex/sqrt.mjs +++ b/src/complex/sqrt.mjs @@ -1,3 +1,4 @@ +import {Returns, returnTypeOf} from '../core/Returns.mjs' export * from './Types/Complex.mjs' export const sqrt = { @@ -12,29 +13,41 @@ export const sqrt = { 'multiply(T,T)': mult, 'self(T)': me, 'divide(T,T)': div, - 'abs(Complex)': absC, + 'absquare(Complex)': absqC, 'subtract(T,T)': sub }) => { + let baseReturns = returnTypeOf(me) + if (baseReturns.includes('|')) { + // Bit of a hack, because it is relying on other implementations + // to list the "typical" value of sqrt first + baseReturns = baseReturns.split('|', 1)[0] + } + if (config.predictable) { - return z => { + return Returns(`Complex<${baseReturns}>`, z => { const reOne = uno(z.re) if (isZ(z.im) && sgn(z.re) === reOne) return cplxU(me(z.re)) const reTwo = plus(reOne, reOne) + const myabs = me(absqC(z)) return cplxB( - mult(sgn(z.im), me(div(plus(absC(z),z.re), reTwo))), - me(div(sub(absC(z),z.re), reTwo)) + mult(sgn(z.im), me(div(plus(myabs, z.re), reTwo))), + me(div(sub(myabs, z.re), reTwo)) ) + }) + } + + return Returns( + `Complex<${baseReturns}>|${baseReturns}|undefined`, + z => { + const reOne = uno(z.re) + if (isZ(z.im) && sgn(z.re) === reOne) return me(z.re) + const reTwo = plus(reOne, reOne) + const myabs = me(absqC(z)) + const reSqrt = me(div(plus(myabs, z.re), reTwo)) + const imSqrt = me(div(sub(myabs, z.re), reTwo)) + if (reSqrt === undefined || imSqrt === undefined) return undefined + return cplxB(mult(sgn(z.im), reSqrt), imSqrt) } - } - return z => { - const reOne = uno(z.re) - if (isZ(z.im) && sgn(z.re) === reOne) return me(z.re) - const reTwo = plus(reOne, reOne) - const reSqrt = me(div(plus(absC(z),z.re), reTwo)) - const imSqrt = me(div(sub(absC(z),z.re), reTwo)) - if (reSqrt === undefined || imSqrt === undefined) return undefined - return cplxB(mult(sgn(z.im), reSqrt), imSqrt) - } + ) } } - diff --git a/src/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index 1add7eb..4b46752 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -1,22 +1,94 @@ /* Core of pocomath: create an instance */ import typed from 'typed-function' -import {dependencyExtractor, generateTypeExtractor} from './extractors.mjs' import {makeChain} from './Chain.mjs' +import {dependencyExtractor, generateTypeExtractor} from './extractors.mjs' +import {Returns, returnTypeOf} from './Returns.mjs' import {typeListOfSignature, typesOfSignature, subsetOfKeys} from './utils.mjs' const anySpec = {} // fixed dummy specification of 'any' type +/* Template/signature parsing stuff; should probably be moved to a + * separate file, but it's a bit interleaved at the moment + */ + const theTemplateParam = 'T' // First pass: only allow this one exact parameter +const restTemplateParam = `...${theTemplateParam}` +const templateCall = `<${theTemplateParam}>` const templateFromParam = 'U' // For defining covariant conversions +/* returns the pair [base, instance] for a template type. If the type + * is not a template, instance is undefined + */ +const templatePattern = /^\s*([^<\s]*)\s*<\s*(\S*)\s*>\s*$/ +function splitTemplate(type) { + if (!(type.includes('<'))) return [type, undefined] + const results = templatePattern.exec(type) + return [results[1], results[2]] +} +/* Returns the instance such that type is template instantiated for that + * instance. + */ +function whichInstance(type, template) { + if (template === theTemplateParam) return type + if (type === template) return '' + if (!(template.includes(templateCall))) { + throw new TypeError( + `Type ${template} is not a template, so can't produce ${type}`) + } + const [typeBase, typeInstance] = splitTemplate(type) + if (!typeInstance) { + throw new TypeError( + `Type ${type} not from a template, so isn't instance of ${template}`) + } + const [tempBase, tempInstance] = splitTemplate(template) + if (typeBase !== tempBase) { + throw new TypeError( + `Type ${type} has wrong top-level base to be instance of ${template}`) + } + return whichInstance(typeInstance, tempInstance) +} +/* Same as above, but for signatures */ +function whichSigInstance(sig, tempsig) { + const sigTypes = typeListOfSignature(sig) + const tempTypes = typeListOfSignature(tempsig) + const sigLength = sigTypes.length + if (sigLength === 0) { + throw new TypeError("No types in signature, so can't determine instance") + } + if (sigLength !== tempTypes.length) { + throw new TypeError(`Signatures ${sig} and ${tempsig} differ in length`) + } + let maybeInstance = whichInstance(sigTypes[0], tempTypes[0]) + for (let i = 1; i < sigLength; ++i) { + const currInstance = whichInstance(sigTypes[i], tempTypes[i]) + if (maybeInstance) { + if (currInstance && currInstance !== maybeInstance) { + throw new TypeError( + `Inconsistent instantiation of ${sig} from ${tempsig}`) + } + } else { + maybeInstance = currInstance + } + } + if (!maybeInstance) { + throw new TypeError( + `Signature ${sig} identical to ${tempsig}, not an instance`) + } + return maybeInstance +} + /* Returns a new signature just like sig but with the parameter replaced by * the type */ -function substituteInSig(sig, parameter, type) { +const upperBounds = /\s*(\S*)\s*:\s*(\w*)\s*/g +function substituteInSignature(signature, parameter, type) { + const sig = signature.replaceAll(upperBounds, '$1') const pattern = new RegExp("\\b" + parameter + "\\b", 'g') return sig.replaceAll(pattern, type) } +let lastWhatToDo = null // used in an infinite descent check + 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 @@ -29,9 +101,15 @@ export default class PocomathInstance { 'install', 'installType', 'instantiateTemplate', + 'isPriorTo', + 'isSubtypeOf', 'joinTypes', 'name', + 'returnTypeOf', + 'resolve', 'self', + 'subtypesOf', + 'supertypesOf', 'Templates', 'typeOf', 'Types', @@ -40,22 +118,47 @@ export default class PocomathInstance { constructor(name) { this.name = name - this._imps = {} + this._imps = {} // Pocomath implementations, with dependencies + this._TFimps = {} // typed-function implementations, dependencies resolved this._affects = {} this._typed = typed.create() this._typed.clear() - this._typed.addTypes([{name: 'ground', test: () => true}]) - /* List of types installed in the instance. We start with just dummies - * for the 'any' type and for type parameters: - */ + // The following is an additional typed-function universe for resolving + // uninstantiated template instances. It is linked to the main one in + // its onMismatch function, below: + this._metaTyped = typed.create() + this._metaTyped.clear() + // And these are the meta bindings: (I think we don't need separate + // invalidation for them as they are only accessed through a main call.) + this._meta = {} // The resulting typed-functions + this._metaTFimps = {} // and their implementations + const me = this + const myTyped = this._typed + this._typed.onMismatch = (name, args, sigs) => { + if (me._invalid.has(name)) { + // rebuild implementation and try again + return me[name](...args) + } + const metaversion = me._meta[name] + if (metaversion) { + return me._meta[name](...args) + } + me._typed.throwMismatchError(name, args, sigs) + } + // 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.Types.ground = anySpec - // All the template types that have been defined + // Types that have been moved into the metaverse: + this._metafiedTypes = new Set() + // All the template types that have been defined: this.Templates = {} - // The actual type testing functions + // And their instantiations: + this._instantiationsOf = {} + // The actual type testing functions: this._typeTests = {} - this._subtypes = {} // For each type, gives all of its (in)direct subtypes + // For each type, gives all of its (in)direct subtypes in topo order: + this._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 @@ -67,25 +170,19 @@ export default class PocomathInstance { this._maxDepthSeen = 1 // deepest template nesting we've actually encountered this._invalid = new Set() // methods that are currently invalid this._config = {predictable: false, epsilon: 1e-12} - const self = this this.config = new Proxy(this._config, { get: (target, property) => target[property], set: (target, property, value) => { if (value !== target[property]) { target[property] = value - self._invalidateDependents('config') + me._invalidateDependents('config') } return true // successful } }) this._plainFunctions = new Set() // the names of the plain functions this._chainRepository = {} // place to store chainified functions - - this._installFunctions({ - typeOf: {ground: {uses: new Set(), does: () => () => 'any'}} - }) - - this.joinTypes = this.joinTypes.bind(this) + this.joinTypes = this.joinTypes.bind(me) } /** @@ -144,7 +241,7 @@ export default class PocomathInstance { * instantiation can be accomplished by prefixin the signature with an * exclamation point. */ - install(ops) { + install = Returns('void', function(ops) { if (ops instanceof PocomathInstance) { return _installInstance(ops) } @@ -170,14 +267,17 @@ export default class PocomathInstance { const stdimps = {} for (const [signature, does] of Object.entries(spec)) { const uses = new Set() - does(dependencyExtractor(uses)) + try { + does(dependencyExtractor(uses)) + } catch { + } stdimps[signature] = {uses, does} } stdFunctions[item] = stdimps } } this._installFunctions(stdFunctions) - } + }) /* Merge any number of PocomathInstances or modules: */ static merge(name, ...pieces) { @@ -188,10 +288,28 @@ export default class PocomathInstance { return result } + /* Determine the return type of an operation given an input signature */ + returnTypeOf = Returns('string', function(operation, signature) { + for (const type of typeListOfSignature(signature)) { + this._maybeInstantiate(type) + } + if (typeof operation !== 'string') { + operation = operation.name + } + const details = this._pocoFindSignature(operation, signature) + if (details) { + return returnTypeOf(details.fn, signature, this) + } + return returnTypeOf(this[operation], signature, this) + }) + /* Return a chain object for this instance with a given value: */ - chain(value) { - return makeChain(value, this, this._chainRepository) - } + chain = Returns( + sig => `Chain<${sig}>`, + function(value) { + return makeChain(value, this, this._chainRepository) + } + ) _installInstance(other) { for (const [type, spec] of Object.entries(other.Types)) { @@ -240,9 +358,9 @@ export default class PocomathInstance { for (const name of requiredSet) { for (const type of typeSet) { try { - const modName = `../${type}/${name}.mjs` - const mod = await import(modName) - this.install(mod) + const moduleName = `../${type}/${name}.mjs` + const module = await import(moduleName) + this.install(module) } catch (err) { if (!(err.message.includes('find'))) { // Not just a error because module doesn't exist @@ -283,16 +401,17 @@ export default class PocomathInstance { * Implementation note: unlike _installFunctions below, we can make * the corresponding changes to the _typed object immediately */ - installType(type, spec) { - const parts = type.split(/[<,>]/) + installType = Returns('void', function(type, spec) { + const parts = type.split(/[<,>]/).map(s => s.trim()) if (this._templateParam(parts[0])) { throw new SyntaxError( `Type name '${type}' reserved for template parameter`) } if (parts.some(this._templateParam.bind(this))) { - // It's a template, deal with it separately + // It's an uninstantiated template, deal with it separately return this._installTemplateType(type, spec) } + const base = parts[0] if (type in this.Types) { if (spec !== this.Types[type]) { throw new SyntaxError(`Conflicting definitions of type ${type}`) @@ -305,7 +424,7 @@ export default class PocomathInstance { } let beforeType = spec.refines if (!beforeType) { - beforeType = 'ground' + beforeType = 'any' for (const other of spec.before || []) { if (other in this.Types) { beforeType = other @@ -321,14 +440,14 @@ export default class PocomathInstance { this._typeTests[type] = testFn this._typed.addTypes([{name: type, test: testFn}], beforeType) this.Types[type] = spec - this._subtypes[type] = new Set() + this._subtypes[type] = [] this._priorTypes[type] = new Set() // Update all the subtype sets of supertypes up the chain let nextSuper = spec.refines while (nextSuper) { this._invalidateDependents(':' + nextSuper) this._priorTypes[nextSuper].add(type) - this._subtypes[nextSuper].add(type) + this._addSubtypeTo(nextSuper, type) nextSuper = this.Types[nextSuper].refines } /* Now add conversions to this type */ @@ -347,6 +466,21 @@ export default class PocomathInstance { for (const subtype of this._subtypes[from]) { this._priorTypes[nextSuper].add(subtype) } + + /* Add the conversion in the metaverse if need be: */ + const [toBase, toInstance] = splitTemplate(nextSuper) + if (toInstance) { + const [fromBase, fromInstance] = splitTemplate(from) + if (!fromBase || fromBase !== toBase) { + this._metafy(from) + try { + this._metaTyped.addConversion( + {from, to: toBase, convert: spec.from[from]}) + } catch { + } + } + } + nextSuper = this.Types[nextSuper].refines } } @@ -356,8 +490,8 @@ export default class PocomathInstance { for (const fromtype in this.Types[to].from) { if (type == fromtype || (fromtype in this._subtypes - && this._subtypes[fromtype].has(type))) { - if (spec.refines == to || spec.refines in this._subtypes[to]) { + && this.isSubtypeOf(type, fromtype))) { + if (spec.refines == to || this.isSubtypeOf(spec.refines,to)) { throw new SyntaxError( `Conversion of ${type} to its supertype ${to} disallowed.`) } @@ -371,6 +505,16 @@ export default class PocomathInstance { convert: this.Types[to].from[fromtype] }) this._invalidateDependents(':' + nextSuper) + /* Add the conversion in the metaverse if need be: */ + const [toBase, toInstance] = splitTemplate(nextSuper) + if (toInstance && base !== toBase) { + this._metafy(type) + this._metaTyped.addConversion({ + from: type, + to: toBase, + convert: this.Types[to].from[fromtype] + }) + } } catch { } this._priorTypes[nextSuper].add(type) @@ -381,43 +525,92 @@ export default class PocomathInstance { } // update the typeOf function const imp = {} - imp[type] = {uses: new Set(), does: () => () => type} + imp[type] = {uses: new Set(), does: () => Returns('string', () => type)} this._installFunctions({typeOf: imp}) + }) + + _metafy(type) { + if (this._metafiedTypes.has(type)) return + this._metaTyped.addTypes([{name: type, test: this._typeTests[type]}]) + this._metafiedTypes.add(type) } - /* Returns the most refined type of all the types in the array, with - * '' standing for the empty type for convenience. If the second + _addSubtypeTo(sup, sub) { + if (this.isSubtypeOf(sub, sup)) return + const supSubs = this._subtypes[sup] + let i + for (i = 0; i < supSubs.length; ++i) { + if (this.isSubtypeOf(sub, supSubs[i])) break + } + supSubs.splice(i, 0, sub) + } + + /* Returns true if typeA is a strict subtype of type B */ + isSubtypeOf = Returns('boolean', function(typeA, typeB) { + // Currently not considering types to be a subtype of 'any' + if (typeB === 'any' || typeA === 'any') return false + return this._subtypes[typeB].includes(typeA) + }) + + /* Returns true if typeA is a subtype of or converts to type B */ + isPriorTo = Returns('boolean', function(typeA, typeB) { + if (!(typeB in this._priorTypes)) return false + return this._priorTypes[typeB].has(typeA) + }) + + /* Returns a list of the strict ubtypes of a given type, in topological + * sorted order (i.e, no type on the list contains one that comes after it). + */ + subtypesOf = Returns('Array', function(type) { + return this._subtypes[type] // should we clone? + }) + + /* Returns a list of the supertypes of a given type, starting with itself, + * in topological order + */ + supertypesOf = Returns('Array', function(type) { + const supList = [] + while (type) { + supList.push(type) + type = this.Types[type].refines + } + return supList + }) + + /* Returns the most refined type containing all the types in the array, + * with '' standing for the empty type for convenience. If the second * argument `convert` is true, a convertible type is considered a * a subtype (defaults to false). */ - joinTypes(types, convert) { + joinTypes = Returns('string', function(types, convert) { let join = '' for (const type of types) { join = this._joinTypes(join, type, convert) } return join - } + }) + /* helper for above */ _joinTypes(typeA, typeB, convert) { if (!typeA) return typeB if (!typeB) return typeA if (typeA === 'any' || typeB === 'any') return 'any' - if (typeA === 'ground' || typeB === 'ground') return 'ground' if (typeA === typeB) return typeA const subber = convert ? this._priorTypes : this._subtypes - if (subber[typeB].has(typeA)) return typeB + const pick = convert ? 'has' : 'includes' + if (subber[typeB][pick](typeA)) return typeB /* OK, so we need the most refined supertype of A that contains B: */ let nextSuper = typeA while (nextSuper) { - if (subber[nextSuper].has(typeB)) return nextSuper + if (subber[nextSuper][pick](typeB)) return nextSuper nextSuper = this.Types[nextSuper].refines } /* And if conversions are allowed, we have to search the other way too */ if (convert) { nextSuper = typeB while (nextSuper) { - if (subber[nextSuper].has(typeA)) return nextSuper + if (subber[nextSuper][pick](typeA)) return nextSuper nextSuper = this.Types[nextSuper].refines } } @@ -427,13 +620,14 @@ export default class PocomathInstance { /* Returns a list of all types that have been mentioned in the * signatures of operations, but which have not actually been installed: */ - undefinedTypes() { - return Array.from(this._seenTypes).filter(t => !(t in this.Types)) - } + undefinedTypes = Returns('Array', function() { + return Array.from(this._seenTypes).filter( + t => !(t in this.Types || t in this.Templates)) + }) /* Used internally to install a template type */ _installTemplateType(type, spec) { - const base = type.split('<')[0] + const [base] = splitTemplate(type) /* For now, just allow a single template per base type; that * might need to change later: */ @@ -444,11 +638,29 @@ export default class PocomathInstance { } return } + + // install the "base type" in the meta universe: + let beforeType = 'any' + for (const other of spec.before || []) { + if (other in this.templates) { + beforeType = other + break + } + } + this._metaTyped.addTypes([{name: base, test: spec.base}], beforeType) + this._instantiationsOf[base] = new Set() + // update the typeOf function const imp = {} - imp[type] = {uses: new Set(['T']), does: ({T}) => () => `${base}<${T}>`} + imp[type] = { + uses: new Set(['T']), + does: ({T}) => Returns('string', () => `${base}<${T}>`) + } this._installFunctions({typeOf: imp}) + // Invalidate any functions that reference this template type: + this._invalidateDependents(':' + base) + // Nothing else actually happens until we match a template parameter this.Templates[base] = {type, spec} } @@ -480,8 +692,27 @@ export default class PocomathInstance { `Conflicting definitions of ${signature} for ${name}`) } } else { - // Must avoid aliasing into another instance: - opImps[signature] = {uses: behavior.uses, does: behavior.does} + /* Check if it's an ordinary non-template signature */ + let explicit = true + for (const type of typesOfSignature(signature)) { + for (const word of type.split(/[<>:\s]/)) { + if (this._templateParam(word)) { + explicit = false + break + } + } + if (!explicit) break + } + opImps[signature] = { + explicit, + resolved: false, + uses: behavior.uses, + does: behavior.does + } + if (!explicit) { + opImps[signature].hasInstantiations = {} + opImps[signature].needsInstantiations = new Set() + } for (const dep of behavior.uses) { const depname = dep.split('(', 1)[0] if (depname === 'self' || this._templateParam(depname)) { @@ -520,11 +751,42 @@ export default class PocomathInstance { * Reset an operation to require creation of typed-function, * and if it has no implementations so far, set them up. */ - _invalidate(name) { - if (this._invalid.has(name)) return + _invalidate(name, reason) { if (!(name in this._imps)) { this._imps[name] = {} + this._TFimps[name] = {} + this._metaTFimps[name] = {} } + if (reason) { + // Make sure no TF imps that depend on reason remain: + for (const [signature, behavior] of Object.entries(this._imps[name])) { + let invalidated = false + if (reason.charAt(0) === ':') { + const badType = reason.slice(1) + if (signature.includes(badType)) invalidated = true + } else { + for (const dep of behavior.uses) { + if (dep.includes(reason)) { + invalidated = true + break + } + } + } + if (invalidated) { + if (behavior.explicit) { + if (behavior.resolved) delete this._TFimps[signature] + behavior.resolved = false + } else { + for (const fullSig + of Object.values(behavior.hasInstantiations)) { + delete this._TFimps[fullSig] + } + behavior.hasInstantiations = {} + } + } + } + } + if (this._invalid.has(name)) return this._invalid.add(name) this._invalidateDependents(name) const self = this @@ -544,7 +806,7 @@ export default class PocomathInstance { _invalidateDependents(name) { if (name in this._affects) { for (const ancestor of this._affects[name]) { - this._invalidate(ancestor) + this._invalidate(ancestor, name) } } } @@ -558,13 +820,15 @@ export default class PocomathInstance { if (!imps) { throw new SyntaxError(`No implementations for ${name}`) } + const tf_imps = this._TFimps[name] + const meta_imps = this._metaTFimps[name] /* Collect the entries we know the types for */ const usableEntries = [] for (const entry of Object.entries(imps)) { let keep = true for (const type of typesOfSignature(entry[0])) { if (type in this.Types) continue - const baseType = type.split('<')[0] + const [baseType] = splitTemplate(type) if (baseType in this.Templates) continue keep = false break @@ -580,78 +844,109 @@ export default class PocomathInstance { * in the midst of being reassembled */ Object.defineProperty(this, name, {configurable: true, value: 'limbo'}) - const tf_imps = {} for (const [rawSignature, behavior] of usableEntries) { - /* Check if it's an ordinary non-template signature */ - let explicit = true - for (const type of typesOfSignature(rawSignature)) { - for (const word of type.split(/[<>]/)) { - if (this._templateParam(word)) { - explicit = false - break - } + if (behavior.explicit) { + if (!(behavior.resolved)) { + this._addTFimplementation(tf_imps, rawSignature, behavior) + tf_imps[rawSignature]._pocoSignature = rawSignature + behavior.resolved = true } - } - 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() + /* First, find any upper bounds on the instantation */ + /* TODO: handle multiple upper bounds */ + upperBounds.lastIndex = 0 + let ubType = upperBounds.exec(rawSignature) + if (ubType) { + ubType = ubType[2] + if (!ubType in this.Types) { + throw new TypeError( + `Unknown type upper bound '${ubType}' in '${rawSignature}'`) + } } + /* First, add the known instantiations, gathering all types needed */ + if (ubType) behavior.needsInstantiations.add(ubType) let instantiationSet = new Set() - for (const instType of behavior.instantiations) { + const ubTypes = new Set() + if (!ubType) { + // Collect all upper-bound types for this signature + for (const othersig in imps) { + const thisUB = upperBounds.exec(othersig) + if (thisUB) ubTypes.add(thisUB[2]) + let basesig = othersig.replaceAll(templateCall, '') + if (basesig !== othersig) { + // A template + const testsig = substituteInSignature( + basesig, theTemplateParam, '') + if (testsig === basesig) { + // that is not also top-level + for (const templateType of typeListOfSignature(basesig)) { + if (templateType.slice(0,3) === '...') { + templateType = templateType.slice(3) + } + ubTypes.add(templateType) + } + } + } + } + } + for (const instType of behavior.needsInstantiations) { instantiationSet.add(instType) - for (const other of this._priorTypes[instType]) { - instantiationSet.add(other) + const otherTypes = + ubType ? this.subtypesOf(instType) : this._priorTypes[instType] + for (const other of otherTypes) { + if (!(this._atOrBelowSomeType(other, ubTypes))) { + instantiationSet.add(other) + } + } + } + + /* Prevent other existing signatures from blocking use of top-level + * templates via conversions: + */ + let baseSignature = rawSignature.replaceAll(templateCall, '') + /* Any remaining template params are top-level */ + const signature = substituteInSignature( + baseSignature, theTemplateParam, 'any') + const hasTopLevel = (signature !== baseSignature) + if (!ubType && hasTopLevel) { + for (const othersig in imps) { + let basesig = othersig.replaceAll(templateCall, '') + const testsig = substituteInSignature( + basesig, theTemplateParam, '') + if (testsig !== basesig) continue // a top-level template + for (let othertype of typeListOfSignature(othersig)) { + if (othertype.slice(0,3) === '...') { + othertype = othertype.slice(3) + } + if (this.Types[othertype] === anySpec) continue + const testType = substituteInSignature( + othertype, theTemplateParam, '') + let otherTypeCollection = [othertype] + if (testType !== othertype) { + const [base] = splitTemplate(othertype) + otherTypeCollection = this._instantiationsOf[base] + } + for (const possibility of otherTypeCollection) { + for (const convtype of this._priorTypes[possibility]) { + if (this.isSubtypeOf(convtype, possibility)) continue + if (!(this._atOrBelowSomeType(convtype, ubTypes))) { + instantiationSet.add(convtype) + } + } + } + } } } for (const instType of instantiationSet) { - if (!(instType in this.Types)) continue - if (this.Types[instType] === anySpec) continue - const signature = - substituteInSig(rawSignature, theTemplateParam, instType) - /* Don't override an explicit implementation: */ - if (signature in imps) continue - /* Don't go too deep */ - let maxdepth = 0 - for (const argType in typeListOfSignature(signature)) { - const depth = argType.split('<').length - if (depth > maxdepth) maxdepth = depth - } - if (maxdepth > this._maxDepthSeen + 1) continue - /* All right, go ahead and instantiate */ - 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] - } - } - return behavior.does(innerRefs) - } - this._addTFimplementation( - tf_imps, signature, {uses, does: patch}) + this._instantiateTemplateImplementation(name, rawSignature, instType) } /* Now add the catchall signature */ - let templateCall = `<${theTemplateParam}>` - /* Relying here that the base of 'Foo' is 'Foo': */ - let baseSignature = rawSignature.replaceAll(templateCall, '') - /* Any remaining template params are top-level */ - const signature = substituteInSig( - baseSignature, theTemplateParam, 'ground') + /* (Not needed if if it's a bounded template) */ + if (ubType) continue + if (behavior.resolved) continue /* The catchall signature has to detect the actual type of the call * and add the new instantiations. * First, prepare the type inference data: @@ -670,148 +965,131 @@ export default class PocomathInstance { throw new SyntaxError( `Cannot find template parameter in ${rawSignature}`) } - /* And eliminate template parameters from the dependencies */ - const simplifiedUses = {} - for (const dep of behavior.uses) { - let [func, needsig] = dep.split(/[()]/) - if (needsig) { - const subsig = substituteInSig(needsig, theTemplateParam, '') - if (subsig === needsig) { - simplifiedUses[dep] = dep - } else { - simplifiedUses[dep] = func - } - } else { - simplifiedUses[dep] = dep - } - } + /* Now build the catchall implementation */ const self = this - const patch = (refs) => (...args) => { - /* We unbundle the rest arg if there is one */ - const regLength = args.length - 1 - if (restParam) { - const restArgs = args.pop() - args = args.concat(restArgs) - } - /* Now infer the type we actually should have been called for */ - let i = -1 - let j = -1 - /* collect the arg types */ - const argTypes = [] - for (const arg of args) { - ++j - // in case of rest parameter, reuse last parameter type: - if (i < inferences.length - 1) ++i - if (inferences[i]) { - const argType = inferences[i](arg) - if (!argType) { - throw TypeError( - `Type inference failed for argument ${j} of ${name}`) - } - if (argType === 'any') { - throw TypeError( - `In call to ${name}, incompatible template arguments: ` - // + args.map(a => JSON.stringify(a)).join(', ') - // unfortunately barfs on bigints. Need a better formatter - // wish we could just use the one that console.log uses; - // is that accessible somehow? - + args.map(a => a.toString()).join(', ') - + ' of types ' + argTypes.join(', ') + argType) - } - argTypes.push(argType) + /* For return type annotation, we may have to fix this to + propagate the return type. At the moment we are just bagging + */ + const patch = () => { + const patchFunc = (...tfBundledArgs) => { + /* We unbundle the rest arg if there is one */ + let args = Array.from(tfBundledArgs) + const regLength = args.length - 1 + if (restParam) { + const restArgs = args.pop() + args = args.concat(restArgs) } - } - if (argTypes.length === 0) { - throw TypeError('Type inference failed for' + name) - } - let usedConversions = false - let instantiateFor = self.joinTypes(argTypes) - if (instantiateFor === 'any') { - usedConversions = true - instantiateFor = self.joinTypes(argTypes, usedConversions) + /* Now infer the type we actually should have been called for */ + let i = -1 + let j = -1 + /* collect the arg types */ + const argTypes = [] + for (const arg of args) { + ++j + // in case of rest parameter, reuse last parameter type: + if (i < inferences.length - 1) ++i + if (inferences[i]) { + const argType = inferences[i](arg) + if (!argType) { + throw TypeError( + `Type inference failed for argument ${j} of ${name}`) + } + if (argType === 'any') { + throw TypeError( + `In call to ${name}, ` + + 'incompatible template arguments:' + // + args.map(a => JSON.stringify(a)).join(', ') + // unfortunately barfs on bigints. Need a better + // formatter. I wish we could just use the one that + // console.log uses; is that accessible somehow? + + args.map(a => a.toString()).join(', ') + + ' of types ' + argTypes.join(', ') + argType) + } + argTypes.push(argType) + } + } + if (argTypes.length === 0) { + throw TypeError('Type inference failed for' + name) + } + let usedConversions = false + let instantiateFor = self.joinTypes(argTypes) if (instantiateFor === 'any') { - throw TypeError( - `In call to ${name}, no type unifies arguments ` - + args.toString() + '; of types ' + argTypes.toString() - + '; note each consecutive pair must unify to a ' - + 'supertype of at least one of them') - } - } - const depth = instantiateFor.split('<').length - if (depth > self._maxDepthSeen) { - self._maxDepthSeen = depth - } - /* Generate the list of actual wanted types */ - const wantTypes = parTypes.map(type => substituteInSig( - type, theTemplateParam, instantiateFor)) - /* Now we have to add any actual types that are relevant - * to this invocation. Namely, that would be every formal parameter - * type in the invocation, with the parameter template instantiated - * by instantiateFor, and for all of instantiateFor's "prior types" - */ - for (j = 0; j < parTypes.length; ++j) { - if (wantTypes[j] !== parTypes[j] && parTypes[j].includes('<')) { - // actually used the param and is a template - self._ensureTemplateTypes(parTypes[j], instantiateFor) - } - } - /* Transform the arguments if we used any conversions: */ - if (usedConversions) { - i = - 1 - for (j = 0; j < args.length; ++j) { - if (i < parTypes.length - 1) ++i - let wantType = parTypes[i] - if (wantType.slice(0,3) === '...') { - wantType = wantType.slice(3) - } - wantType = substituteInSig( - wantType, theTemplateParam, instantiateFor) - if (wantType !== parTypes[i]) { - args[j] = self._typed.convert(args[j], wantType) + usedConversions = true + instantiateFor = self.joinTypes(argTypes, usedConversions) + if (instantiateFor === 'any') { + throw TypeError( + `In call to ${name}, no type unifies arguments ` + + args.toString() + '; of types ' + argTypes.toString() + + '; note each consecutive pair must unify to a ' + + 'supertype of at least one of them') } } - } - /* Finally reassemble the rest args if there were any */ - if (restParam) { - const restArgs = args.slice(regLength) - args = args.slice(0,regLength) - args.push(restArgs) - } - /* Arrange that the desired instantiation will be there next - * time so we don't have to go through that again for this type - */ - refs[theTemplateParam] = instantiateFor - behavior.instantiations.add(instantiateFor) - self._invalidate(name) - // And update refs because we now know the type we're instantiating - // for: - const innerRefs = {} - for (const dep in simplifiedUses) { - const simplifiedDep = simplifiedUses[dep] - if (dep === simplifiedDep) { - innerRefs[dep] = refs[dep] - } else { - let [func, needsig] = dep.split(/[()]/) - if (self._typed.isTypedFunction(refs[simplifiedDep])) { - const subsig = substituteInSig( - needsig, theTemplateParam, instantiateFor) - let resname = simplifiedDep - if (resname == 'self') resname = name - innerRefs[dep] = self._pocoresolve( - resname, subsig, refs[simplifiedDep]) - } else { - innerRefs[dep] = refs[simplifiedDep] + const depth = instantiateFor.split('<').length + if (depth > self._maxDepthSeen) { + self._maxDepthSeen = depth + } + /* Generate the list of actual wanted types */ + const wantTypes = parTypes.map(type => substituteInSignature( + type, theTemplateParam, instantiateFor)) + const wantSig = wantTypes.join(',') + /* Now we have to add any actual types that are relevant + * to this invocation. Namely, that would be every formal + * parameter type in the invocation, with the parameter + * template instantiated by instantiateFor, and for all of + * instantiateFor's "prior types" + */ + for (j = 0; j < parTypes.length; ++j) { + if (wantTypes[j] !== parTypes[j] && parTypes[j].includes('<')) { + // actually used the param and is a template + self._ensureTemplateTypes(parTypes[j], instantiateFor) } } + + /* Request the desired instantiation: */ + // But possibly since this resolution was grabbed, the proper + // instantiation has been added (like if there are multiple + // uses in the implementation of another method. + if (!(behavior.needsInstantiations.has(instantiateFor))) { + behavior.needsInstantiations.add(instantiateFor) + self._invalidate(name) + } + const brandNewMe = self[name] + const whatToDo = self._typed.resolve(brandNewMe, args) + // We can access return type information here + // And in particular, if it might be a template, we should try to + // instantiate it: + const returnType = returnTypeOf(whatToDo.fn, wantSig, self) + for (const possibility of returnType.split('|')) { + const instantiated = self._maybeInstantiate(possibility) + if (instantiated) { + const [tempBase] = splitTemplate(instantiated) + self._invalidateDependents(':' + tempBase) + } + } + if (whatToDo === lastWhatToDo) { + throw new Error( + `Infinite recursion in resolving $name called on` + + args.map(x => x.toString()).join(',')) + } + lastWhatToDo = whatToDo + const retval = whatToDo.implementation(...args) + lastWhatToDo = null + return retval } - // Finally ready to make the call. - return behavior.does(innerRefs)(...args) + Object.defineProperty( + patchFunc, 'name', {value: `${name}(${signature})`}) + patchFunc._pocoSignature = rawSignature + return patchFunc } - // The actual uses value needs to be a set: - const outerUses = new Set(Object.values(simplifiedUses)) + Object.defineProperty( + patch, 'name', {value: `generate[${name}(${signature})]`}) + // TODO?: Decorate patch with a function that calculates the + // correct return type a priori. Deferring because unclear what + // aspects will be merged into typed-function. this._addTFimplementation( - tf_imps, signature, {uses: outerUses, does: patch}) + meta_imps, signature, {uses: new Set(), does: patch}) + behavior.resolved = true } this._correctPartialSelfRefs(name, tf_imps) // Make sure we have all of the needed (template) types; and if they @@ -820,35 +1098,140 @@ export default class PocomathInstance { const badSigs = new Set() for (const sig in tf_imps) { for (const type of typeListOfSignature(sig)) { - if (type.includes('<')) { - // it's a template type, turn it into a template and an arg - let base = type.split('<',1)[0] - const arg = type.slice(base.length+1, -1) - if (base.slice(0,3) === '...') { - base = base.slice(3) - } - if (this.instantiateTemplate(base, arg) === undefined) { - badSigs.add(sig) - } + if (this._maybeInstantiate(type) === undefined) { + badSigs.add(sig) } } } for (const badSig of badSigs) { + const imp = tf_imps[badSig] delete tf_imps[badSig] + const fromBehavior = this._imps[name][imp._pocoSignature] + if (fromBehavior.explicit) { + fromBehavior.resolved = false + } else { + delete fromBehavior.hasInstantiations[imp._pocoInstance] + } } - const tf = this._typed(name, tf_imps) + let tf + if (Object.keys(tf_imps).length > 0) { + tf = this._typed(name, tf_imps) + tf.fromInstance = this + } + let metaTF + if (Object.keys(meta_imps).length > 0) { + metaTF = this._metaTyped(name, meta_imps) + metaTF.fromInstance = this + } + this._meta[name] = metaTF + + tf = tf || metaTF Object.defineProperty(this, name, {configurable: true, value: tf}) return tf } + /* Takes a type and a set of types and returns true if the type + * is a subtype of some type in the set. + */ + _atOrBelowSomeType(type, typeSet) { + if (typeSet.has(type)) return true + let belowSome = false + for (const anUB of typeSet) { + if (anUB in this.Templates) { + if (type.slice(0, anUB.length) === anUB) { + belowSome = true + break + } + } else { + if (this.isSubtypeOf(type, anUB)) { + belowSome = true + break + } + } + } + return belowSome + } + + /* Takes an arbitrary type and performs an instantiation if necessary. + * @param {string} type The type to instantiate + * @param {string | bool | undefined } + * Returns the name of the type if an instantiation occurred, false + * if the type was already present, and undefined if the type can't + * be satisfied (because it is not the name of a type or it is nested + * too deep. + */ + _maybeInstantiate(type) { + if (type.slice(0,3) === '...') { + type = type.slice(3) + } + if (!(type.includes('<'))) { + // Not a template, so just check if type exists + if (type in this.Types) return false // already there + return undefined // no such type + } + // it's a template type, turn it into a template and an arg + let [base, arg] = splitTemplate(type) + return this.instantiateTemplate(base, arg) + } + + /* Generate and include a template instantiation for operation name + * for the template signature templateSignature instantiated for + * instanceType, returning the resulting implementation. + */ + _instantiateTemplateImplementation(name, templateSignature, instanceType) { + if (!(instanceType in this.Types)) return undefined + if (this.Types[instanceType] === anySpec) return undefined + const imps = this._imps[name] + const behavior = imps[templateSignature] + if (instanceType in behavior.hasInstantiations) return undefined + const signature = substituteInSignature( + templateSignature, theTemplateParam, instanceType) + /* Don't override an explicit implementation: */ + if (signature in imps) return undefined + /* Don't go too deep */ + let maxdepth = 0 + for (const argType in typeListOfSignature(signature)) { + const depth = argType.split('<').length + if (depth > maxdepth) maxdepth = depth + } + if (maxdepth > this._maxDepthSeen + 1) return undefined + /* All right, go ahead and instantiate */ + const uses = new Set() + for (const dep of behavior.uses) { + if (this._templateParam(dep)) continue + uses.add(substituteInSignature(dep, theTemplateParam, instanceType)) + } + const patch = (refs) => { + const innerRefs = {} + for (const dep of behavior.uses) { + if (this._templateParam(dep)) { + innerRefs[dep] = instanceType + } else { + const outerName = substituteInSignature( + dep, theTemplateParam, instanceType) + innerRefs[dep] = refs[outerName] + } + } + return behavior.does(innerRefs) + } + const tf_imps = this._TFimps[name] + this._addTFimplementation(tf_imps, signature, {uses, does: patch}) + tf_imps[signature]._pocoSignature = templateSignature + tf_imps[signature]._pocoInstance = instanceType + behavior.hasInstantiations[instanceType] = signature + return tf_imps[signature] + } + /* Adapts Pocomath-style behavior specification (uses, does) for signature - * to typed-function implementations and inserts the result into plain object - * imps + * 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() + const implementation = does() + // could do something with return type information here + imps[signature] = implementation return } const refs = {} @@ -860,7 +1243,7 @@ export default class PocomathInstance { * Verify that the desired signature has been fully grounded: */ if (needsig) { - const trysig = substituteInSig(needsig, theTemplateParam, '') + const trysig = substituteInSignature(needsig, theTemplateParam, '') if (trysig !== needsig) { throw new Error( 'Attempt to add a template implementation: ' + @@ -869,13 +1252,23 @@ export default class PocomathInstance { } 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) + /* Maybe we can resolve the self reference without troubling + * typed-function: + */ + if (needsig in imps && typeof imps[needsig] == 'function') { + refs[dep] = imps[needsig] + } else { + if (full_self_referential) { + throw new SyntaxError( + 'typed-function does not support mixed full and ' + + 'partial self-reference') + } + const needTypes = typesOfSignature(needsig) + const mergedTypes = Object.assign( + {}, this.Types, this.Templates) + if (subsetOfKeys(needTypes, mergedTypes)) { + part_self_references.push(needsig) + } } } else { if (part_self_references.length) { @@ -887,19 +1280,50 @@ export default class PocomathInstance { } } 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 + /* We are in the midst of bundling func */ + let fallback = true + /* So the first thing we can do is try the tf_imps we are + * accumulating: */ - const self = this - refs[dep] = function () { // is this the most efficient? - return self[func].apply(this, arguments) + if (needsig) { + let typedUniverse + let tempTF + if (Object.keys(this._TFimps[func]).length > 0) { + typedUniverse = this._typed + tempTF = typedUniverse('dummy_' + func, this._TFimps[func]) + } else { + typedUniverse = this._metaTyped + tempTF = typedUniverse( + 'dummy_' + func, this._metaTFimps[func]) + } + let result = undefined + try { + result = typedUniverse.find(tempTF, needsig, {exact: true}) + } catch {} + if (result) { + refs[dep] = result + fallback = false + } + } + if (fallback) { + /* Either we need the whole function or the signature + * we need is not available yet, so we 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 + const redirect = function () { // is this the most efficient? + return self[func].apply(this, arguments) + } + Object.defineProperty(redirect, 'name', {value: func}) + Object.defineProperty(redirect, 'fromInstance', {value: this}) + refs[dep] = redirect } } else { // can bundle up func, and grab its signature if need be let destination = this[func] - if (destination &&needsig) { - destination = this._pocoresolve(func, needsig) + if (destination && needsig) { + destination = this.resolve(func, needsig) } refs[dep] = destination } @@ -908,7 +1332,11 @@ export default class PocomathInstance { if (full_self_referential) { imps[signature] = this._typed.referToSelf(self => { refs.self = self - return does(refs) + const implementation = does(refs) + Object.defineProperty(implementation, 'name', {value: does.name}) + implementation.fromInstance = this + // What are we going to do with the return type info in here? + return implementation }) return } @@ -924,48 +1352,88 @@ export default class PocomathInstance { deferred: true, builtRefs: refs, sigDoes: does, + fromInstance: this, psr: part_self_references } return } - imps[signature] = does(refs) + const implementation = does(refs) + implementation.fromInstance = this + // could do something with return type information here? + imps[signature] = implementation } _correctPartialSelfRefs(name, imps) { for (const aSignature in imps) { if (!(imps[aSignature].deferred)) continue - const part_self_references = imps[aSignature].psr + const deferral = imps[aSignature] + const part_self_references = deferral.psr const corrected_self_references = [] + const remaining_self_references = [] + const refs = deferral.builtRefs for (const neededSig of part_self_references) { // Have to find a match for neededSig among the other signatures // of this function. That's a job for typed-function, but we will // try here: if (neededSig in imps) { // the easy case corrected_self_references.push(neededSig) + remaining_self_references.push(neededSig) continue } // No exact match, try to get one that matches with // subtypes since the whole conversion thing in typed-function // is too complicated to reproduce - const foundSig = this._findSubtypeImpl(name, imps, neededSig) + let foundSig = this._findSubtypeImpl(name, imps, neededSig) if (foundSig) { corrected_self_references.push(foundSig) + remaining_self_references.push(neededSig) } else { - throw new Error( - 'Implement inexact self-reference in typed-function for ' - + `${name}(${neededSig})`) + // Maybe it's a template instance we don't yet have + foundSig = this._findSubtypeImpl( + name, this._imps[name], neededSig) + if (foundSig) { + const match = this._pocoFindSignature(name, neededSig) + const neededTemplate = match.fn._pocoSignature + const neededInstance = whichSigInstance( + neededSig, neededTemplate) + const neededImplementation = + this._instantiateTemplateImplementation( + name, neededTemplate, neededInstance) + if (!neededImplementation) { + refs[`self(${neededSig})`] = match.implementation + } else { + if (typeof neededImplementation === 'function') { + refs[`self(${neededSig})`] = neededImplementation + } else { + corrected_self_references.push(neededSig) + remaining_self_references.push(neededSig) + } + } + } else { + throw new Error( + 'Implement inexact self-reference in typed-function for ' + + `${name}(${neededSig})`) + } } } - const refs = imps[aSignature].builtRefs - const does = imps[aSignature].sigDoes - imps[aSignature] = this._typed.referTo( - ...corrected_self_references, (...impls) => { - for (let i = 0; i < part_self_references.length; ++i) { - refs[`self(${part_self_references[i]})`] = impls[i] + const does = deferral.sigDoes + if (remaining_self_references.length > 0) { + imps[aSignature] = this._typed.referTo( + ...corrected_self_references, (...impls) => { + for (let i = 0; i < remaining_self_references.length; ++i) { + refs[`self(${remaining_self_references[i]})`] = impls[i] + } + const implementation = does(refs) + // What will we do with the return type info in here? + return implementation } - return does(refs) - } - ) + ) + } else { + imps[aSignature] = does(refs) + } + imps[aSignature]._pocoSignature = deferral._pocoSignature + imps[aSignature]._pocoInstance = deferral._pocoInstance + imps[aSignature].fromInstance = deferral.fromInstance } } @@ -974,8 +1442,7 @@ export default class PocomathInstance { * in the instance. */ _ensureTemplateTypes(template, type) { - const base = template.split('<', 1)[0] - const arg = template.slice(base.length + 1, -1) + const [base, arg] = splitTemplate(template) if (!arg) { throw new Error( 'Implementation error in _ensureTemplateTypes', template, type) @@ -995,12 +1462,13 @@ export default class PocomathInstance { return resultingTypes } - /* Maybe add the instantiation of template type base with argument tyoe - * instantiator to the Types of this instance, if it hasn't happened already. + /* Maybe add the instantiation of template type base with argument type + * instantiator to the Types of this instance, if it hasn't happened + * already. * Returns the name of the type if added, false if it was already there, * and undefined if the type is declined (because of being nested too deep). */ - instantiateTemplate(base, instantiator) { + instantiateTemplate = Returns('void', function(base, instantiator) { const depth = instantiator.split('<').length if (depth > this._maxDepthSeen ) { // don't bother with types much deeper thant we have seen @@ -1010,7 +1478,7 @@ export default class PocomathInstance { if (wantsType in this.Types) return false // OK, need to generate the type from the template // Set up refines, before, test, and from - const newTypeSpec = {refines: base} + const newTypeSpec = {} const maybeFrom = {} const template = this.Templates[base].spec if (!template) { @@ -1018,6 +1486,11 @@ export default class PocomathInstance { `Implementor error in instantiateTemplate(${base}, ${instantiator})`) } const instantiatorSpec = this.Types[instantiator] + if (instantiatorSpec.refines) { + this.instantiateTemplate(base, instantiatorSpec.refines) + // Assuming our templates are covariant, I guess + newTypeSpec.refines = `${base}<${instantiatorSpec.refines}>` + } let beforeTypes = [] if (instantiatorSpec.before) { beforeTypes = instantiatorSpec.before.map(type => `${base}<${type}>`) @@ -1025,36 +1498,35 @@ export default class PocomathInstance { if (template.before) { for (const beforeTmpl of template.before) { beforeTypes.push( - substituteInSig(beforeTmpl, theTemplateParam, instantiator)) + substituteInSignature(beforeTmpl, theTemplateParam, instantiator)) } } if (beforeTypes.length > 0) { newTypeSpec.before = beforeTypes } - newTypeSpec.test = template.test(this._typeTests[instantiator]) + const templateTest = template.test(this._typeTests[instantiator]) + newTypeSpec.test = x => (template.base(x) && templateTest(x)) if (template.from) { for (let source in template.from) { - const instSource = substituteInSig( + const instSource = substituteInSignature( source, theTemplateParam, instantiator) - let usesFromParam = false - for (const word of instSource.split(/[<>]/)) { - if (word === templateFromParam) { - usesFromParam = true - break - } - } + const testSource = substituteInSignature( + instSource, templateFromParam, instantiator) + const usesFromParam = (testSource !== instSource) if (usesFromParam) { for (const iFrom in instantiatorSpec.from) { - const finalSource = substituteInSig( + const finalSource = substituteInSignature( instSource, templateFromParam, iFrom) maybeFrom[finalSource] = template.from[source]( instantiatorSpec.from[iFrom]) } - // Assuming all templates are covariant here, I guess... - for (const subType of this._subtypes[instantiator]) { - const finalSource = substituteInSig( - instSource, templateFromParam, subType) - maybeFrom[finalSource] = template.from[source](x => x) + if (testSource !== wantsType) { // subtypes handled with refines + // Assuming all templates are covariant here, I guess... + for (const subType of this._subtypes[instantiator]) { + const finalSource = substituteInSignature( + instSource, templateFromParam, subType) + maybeFrom[finalSource] = template.from[source](x => x) + } } } else { maybeFrom[instSource] = template.from[source] @@ -1066,8 +1538,9 @@ export default class PocomathInstance { newTypeSpec.from = maybeFrom } this.installType(wantsType, newTypeSpec) + this._instantiationsOf[base].add(wantsType) return wantsType - } + }) _findSubtypeImpl(name, imps, neededSig) { if (neededSig in imps) return neededSig @@ -1077,42 +1550,48 @@ export default class PocomathInstance { const otherTypeList = typeListOfSignature(otherSig) if (typeList.length !== otherTypeList.length) continue let allMatch = true + let paramBound = 'any' for (let k = 0; k < typeList.length; ++k) { let myType = typeList[k] let otherType = otherTypeList[k] if (otherType === theTemplateParam) { - otherTypeList[k] = 'ground' - otherType = 'ground' + otherTypeList[k] = paramBound + otherType = paramBound } - if (otherType === '...T') { - otherTypeList[k] = '...ground' - otherType = 'ground' + if (otherType === restTemplateParam) { + otherTypeList[k] = `...${paramBound}` + otherType = paramBound } - const adjustedOtherType = otherType.replaceAll( - `<${theTemplateParam}>`, '') + const adjustedOtherType = otherType.replaceAll(templateCall, '') if (adjustedOtherType !== otherType) { otherTypeList[k] = adjustedOtherType otherType = adjustedOtherType } if (myType.slice(0,3) === '...') myType = myType.slice(3) if (otherType.slice(0,3) === '...') otherType = otherType.slice(3) + const otherBound = upperBounds.exec(otherType) + if (otherBound) { + paramBound = otherBound[2] + otherType = paramBound + otherTypeList[k] = otherBound[1].replaceAll( + theTemplateParam, paramBound) + } if (otherType === 'any') continue - if (otherType === 'ground') continue - if (!(otherType in this.Types)) { - allMatch = false - break - } - if (myType === otherType - || this._subtypes[otherType].has(myType)) { - continue - } + if (myType === otherType) continue if (otherType in this.Templates) { + const [myBase] = splitTemplate(myType) + if (myBase === otherType) continue if (this.instantiateTemplate(otherType, myType)) { let dummy dummy = this[name] // for side effects return this._findSubtypeImpl(name, this._imps[name], neededSig) } } + if (!(otherType in this.Types)) { + allMatch = false + break + } + if (this.isSubtypeOf(myType, otherType)) continue allMatch = false break } @@ -1124,28 +1603,96 @@ export default class PocomathInstance { return foundSig } - _pocoresolve(name, sig, typedFunction) { + _pocoFindSignature(name, sig, typedFunction) { if (!this._typed.isTypedFunction(typedFunction)) { typedFunction = this[name] } - let result = undefined - try { - result = this._typed.find(typedFunction, sig, {exact: true}) - } catch { + const haveTF = this._typed.isTypedFunction(typedFunction) + if (haveTF) { + // First try a direct match + let result + try { + result = this._typed.findSignature(typedFunction, sig, {exact: true}) + } catch { + } + if (result) return result + // Next, look ourselves but with subtypes: + const wantTypes = typeListOfSignature(sig) + for (const [implSig, details] + of typedFunction._typedFunctionData.signatureMap) { + let allMatched = true + const implTypes = typeListOfSignature(implSig) + for (let i = 0; i < wantTypes.length; ++i) { + const implIndex = Math.min(i, implTypes.length - 1) + let implType = implTypes[implIndex] + if (implIndex < i) { + if (implType.slice(0,3) !== '...') { + // ran out of arguments in impl + allMatched = false + break + } + } + if (implType.slice(0,3) === '...') { + implType = implType.slice(3) + } + const hasMatch = implType.split('|').some( + t => (wantTypes[i] === t || this.isSubtypeOf(wantTypes[i], t))) + if (!hasMatch) { + allMatched = false + break + } + } + if (allMatched) return details + } } - if (result) return result + if (!(this._imps[name])) return undefined const foundsig = this._findSubtypeImpl(name, this._imps[name], sig) - if (foundsig) return this._typed.find(typedFunction, foundsig) - // Make sure bundle is up-to-date: + if (foundsig) { + if (haveTF) { + try { + return this._typed.findSignature(typedFunction, foundsig) + } catch { + } + } + try { + return this._metaTyped.findSignature(this._meta[name], foundsig) + } catch { + } + // We have an implementation but not a typed function. Do the best + // we can: + const foundImpl = this._imps[name][foundsig] + const needs = {} + for (const dep of foundImpl.uses) { + const [base, sig] = dep.split('()') + needs[dep] = this.resolve(base, sig) + } + const pseudoImpl = foundImpl.does(needs) + return {fn: pseudoImpl, implementation: pseudoImpl} + } + // Hmm, no luck. Make sure bundle is up-to-date and retry: + let result = undefined typedFunction = this[name] try { - result = this._typed.find(typedFunction, sig) + result = this._typed.findSignature(typedFunction, sig) } catch { } - if (result) return result + return result + } + + /* Returns a function that implements the operation with the given name + * when called with the given signature. The optional third argument is + * the typed function that provides the operation name, which can be + * passed in for efficiency if it is already available. + */ + resolve = Returns('function', function (name, sig, typedFunction) { + if (!this._typed.isTypedFunction(typedFunction)) { + typedFunction = this[name] + } + const result = this._pocoFindSignature(name, sig, typedFunction) + if (result) return result.implementation // total punt, revert to typed-function resolution on every call; // hopefully this happens rarely: return typedFunction - } + }) } diff --git a/src/core/Returns.mjs b/src/core/Returns.mjs new file mode 100644 index 0000000..211babd --- /dev/null +++ b/src/core/Returns.mjs @@ -0,0 +1,34 @@ +/* Annotate a function with its return type */ + +/* Unfortunately JavaScript is missing a way to cleanly clone a function + * object, see https://stackoverflow.com/questions/1833588 + */ + +const clonedFrom = Symbol('the original function this one was cloned from') +function cloneFunction(fn) { + const behavior = fn[clonedFrom] || fn // don't nest clones + const theClone = function () { return behavior.apply(this, arguments) } + Object.assign(theClone, fn) + theClone[clonedFrom] = body + Object.defineProperty( + theClone, 'name', {value: fn.name, configurable: true }) + return theClone +} + +export function Returns(type, fn) { + if ('returns' in fn) fn = cloneFunction(fn) + fn.returns = type + return fn +} + +export function returnTypeOf(fn, signature, pmInstance) { + const typeOfReturns = typeof fn.returns + if (typeOfReturns === 'undefined') return 'any' + if (typeOfReturns === 'string') return fn.returns + // not sure if we will need a function to determine the return type, + // but allow it for now: + return fn.returns(signature, pmInstance) +} + +export default Returns + diff --git a/src/core/utils.mjs b/src/core/utils.mjs index db164dd..9ae9aab 100644 --- a/src/core/utils.mjs +++ b/src/core/utils.mjs @@ -8,6 +8,8 @@ export function subsetOfKeys(set, obj) { /* Returns a list of the types mentioned in a typed-function signature */ export function typeListOfSignature(signature) { + signature = signature.trim() + if (!signature) return [] return signature.split(',').map(s => s.trim()) } diff --git a/src/generic/Types/adapted.mjs b/src/generic/Types/adapted.mjs index ba20889..6c54fc5 100644 --- a/src/generic/Types/adapted.mjs +++ b/src/generic/Types/adapted.mjs @@ -1,4 +1,6 @@ import PocomathInstance from '../../core/PocomathInstance.mjs' +import Returns from '../../core/Returns.mjs' + /* creates a PocomathInstance incorporating a new numeric type encapsulated * as a class. (This instance can the be `install()ed` in another to add the * type so created.) @@ -22,15 +24,15 @@ export default function adapted(name, Thing, overrides) { // first a creator function, with name depending on the name of the thing: const creatorName = overrides.creatorName || name.toLowerCase() const creator = overrides[creatorName] - ? overrides[creatorName]('') + ? overrides[creatorName][''] : Thing[creatorName] ? (Thing[creatorName]) : ((...args) => new Thing(...args)) const defaultCreatorImps = { - '': () => () => creator(), - '...any': () => args => creator(...args) + '': () => Returns(name, () => creator()), + '...any': () => Returns(name, args => creator(...args)) } - defaultCreatorImps[name] = () => x => x // x.clone(x)? + defaultCreatorImps[name] = () => Returns(name, x => x) // x.clone(x)? operations[creatorName] = overrides[creatorName] || defaultCreatorImps // We make the default instance, just as a place to check for methods @@ -47,34 +49,35 @@ export default function adapted(name, Thing, overrides) { negate: 'neg' } const binaryOps = { - add: 'add', - compare: 'compare', - divide: 'div', - equalTT: 'equals', - gcd: 'gcd', - lcm: 'lcm', - mod: 'mod', - multiply: 'mul', - subtract: 'sub' + add: ['add', name], + compare: ['compare', name], + divide: ['div', name], + equalTT: ['equals', 'boolean'], + gcd: ['gcd', name], + lcm: ['lcm', name], + mod: ['mod', name], + multiply: ['mul', name], + subtract: ['sub', name] } for (const [mathname, standardname] of Object.entries(unaryOps)) { if (standardname in instance) { operations[mathname] = {} - operations[mathname][name] = () => t => t[standardname]() + operations[mathname][name] = () => Returns(name, t => t[standardname]()) } } operations.zero = {} - operations.zero[name] = () => t => creator() + operations.zero[name] = () => Returns(name, t => creator()) operations.one = {} - operations.one[name] = () => t => creator(1) + operations.one[name] = () => Returns(name, t => creator(1)) operations.conjugate = {} - operations.conjugate[name] = () => t => t // or t.clone() ?? + operations.conjugate[name] = () => Returns(name, t => t) // or t.clone() ?? const binarySignature = `${name},${name}` - for (const [mathname, standardname] of Object.entries(binaryOps)) { - if (standardname in instance) { + for (const [mathname, spec] of Object.entries(binaryOps)) { + if (spec[0] in instance) { operations[mathname] = {} - operations[mathname][binarySignature] = () => (t,u) => t[standardname](u) + operations[mathname][binarySignature] = () => Returns( + spec[1], (t,u) => t[spec[0]](u)) } } if ('operations' in overrides) { diff --git a/src/generic/abs.mjs b/src/generic/abs.mjs index 84ebc31..ff20ee8 100644 --- a/src/generic/abs.mjs +++ b/src/generic/abs.mjs @@ -1,7 +1,9 @@ +import Returns from '../core/Returns.mjs' export const abs = { T: ({ + T, 'smaller(T,T)': lt, 'negate(T)': neg, 'zero(T)': zr - }) => t => (smaller(t, zr(t)) ? neg(t) : t) + }) => Returns(T, t => (smaller(t, zr(t)) ? neg(t) : t)) } diff --git a/src/generic/absquare.mjs b/src/generic/absquare.mjs index 26d6717..052131d 100644 --- a/src/generic/absquare.mjs +++ b/src/generic/absquare.mjs @@ -1,6 +1,9 @@ +import Returns from '../core/Returns.mjs' + export const absquare = { T: ({ + T, 'square(T)': sq, 'abs(T)': abval - }) => t => sq(abval(t)) + }) => Returns(T, t => sq(abval(t))) } diff --git a/src/generic/all.mjs b/src/generic/all.mjs index 7132944..45bd9d0 100644 --- a/src/generic/all.mjs +++ b/src/generic/all.mjs @@ -1,5 +1,6 @@ import {adapted} from './Types/adapted.mjs' import Fraction from 'fraction.js/bigfraction.js' +import Returns from '../core/Returns.mjs' export * from './arithmetic.mjs' export * from './relational.mjs' @@ -8,15 +9,18 @@ export const fraction = adapted('Fraction', Fraction, { before: ['Complex'], from: {number: n => new Fraction(n)}, operations: { - compare: {'Fraction,Fraction': () => (f,g) => new Fraction(f.compare(g))}, + compare: { + 'Fraction,Fraction': () => Returns( + 'Fraction', (f,g) => new Fraction(f.compare(g))) + }, mod: { - 'Fraction,Fraction': () => (n,d) => { + 'Fraction,Fraction': () => Returns('Fraction', (n,d) => { // patch for "mathematician's modulus" // OK to use full public API of Fraction here const fmod = n.mod(d) if (fmod.s === -1n) return fmod.add(d.abs()) return fmod - } + }) } } }) diff --git a/src/generic/divide.mjs b/src/generic/divide.mjs index 1aee89b..ab1e893 100644 --- a/src/generic/divide.mjs +++ b/src/generic/divide.mjs @@ -1,7 +1,10 @@ +import Returns from '../core/Returns.mjs' + export const divide = { 'T,T': ({ + T, 'multiply(T,T)': multT, 'invert(T)': invT - }) => (x, y) => multT(x, invT(y)) + }) => Returns(T, (x, y) => multT(x, invT(y))) } diff --git a/src/generic/gcdType.mjs b/src/generic/gcdType.mjs index 1ca16ab..ee86b50 100644 --- a/src/generic/gcdType.mjs +++ b/src/generic/gcdType.mjs @@ -1,3 +1,5 @@ +import Returns from '../core/Returns.mjs' + /* 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. @@ -7,14 +9,14 @@ export default function(type) { const producer = refs => { const modder = refs[`mod(${type},${type})`] const zeroTester = refs[`isZero(${type})`] - return (a,b) => { + return Returns(type, (a,b) => { while (!zeroTester(b)) { const r = modder(a,b) a = b b = r } return a - } + }) } const retval = {} retval[`${type},${type}`] = producer diff --git a/src/generic/identity.mjs b/src/generic/identity.mjs index 2422d2f..fb3853e 100644 --- a/src/generic/identity.mjs +++ b/src/generic/identity.mjs @@ -1,3 +1,11 @@ -export function identity(x) { - return x +import Returns from '../core/Returns.mjs' + +export function identityType(type) { + return () => Returns(type, x => x) } + +export function identitySubTypes(type) { + return ({T}) => Returns(T, x => x) +} + +export const identity = {T: ({T}) => Returns(T, x => x)} diff --git a/src/generic/lcm.mjs b/src/generic/lcm.mjs index 04e78b5..26bfbf8 100644 --- a/src/generic/lcm.mjs +++ b/src/generic/lcm.mjs @@ -1,10 +1,12 @@ +import Returns from '../core/Returns.mjs' import {reducingOperation} from './reducingOperation.mjs' export const lcm = { 'T,T': ({ + T, 'multiply(T,T)': multT, 'quotient(T,T)': quotT, 'gcd(T,T)': gcdT - }) => (a,b) => multT(quotT(a, gcdT(a,b)), b) + }) => Returns(T, (a,b) => multT(quotT(a, gcdT(a,b)), b)) } Object.assign(lcm, reducingOperation) diff --git a/src/generic/mean.mjs b/src/generic/mean.mjs index d12c21b..58cbc19 100644 --- a/src/generic/mean.mjs +++ b/src/generic/mean.mjs @@ -1,3 +1,8 @@ +import Returns from '../core/Returns.mjs' export const mean = { - '...any': ({add, divide}) => args => divide(add(...args), args.length) + '...T': ({ + T, + add, + 'divide(T,NumInt)': div + }) => Returns(T, args => div(add(...args), args.length)) } diff --git a/src/generic/mod.mjs b/src/generic/mod.mjs index 84af4e6..e1b5ec6 100644 --- a/src/generic/mod.mjs +++ b/src/generic/mod.mjs @@ -1,7 +1,10 @@ +import Returns from '../core/Returns.mjs' + export const mod = { 'T,T': ({ + T, 'subtract(T,T)': subT, 'multiply(T,T)': multT, 'quotient(T,T)': quotT - }) => (a,m) => subT(a, multT(m, quotT(a,m))) + }) => Returns(T, (a,m) => subT(a, multT(m, quotT(a,m)))) } diff --git a/src/generic/quotient.mjs b/src/generic/quotient.mjs index 54e000a..521bd2a 100644 --- a/src/generic/quotient.mjs +++ b/src/generic/quotient.mjs @@ -1,3 +1,9 @@ +import Returns from '../core/Returns.mjs' + export const quotient = { - 'T,T': ({'floor(T)': flr, 'divide(T,T)':div}) => (n,d) => flr(div(n,d)) + 'T,T': ({ + T, + 'floor(T)': flr, + 'divide(T,T)': div + }) => Returns(T, (n,d) => flr(div(n,d))) } diff --git a/src/generic/reducingOperation.mjs b/src/generic/reducingOperation.mjs index e29baf1..3c256f2 100644 --- a/src/generic/reducingOperation.mjs +++ b/src/generic/reducingOperation.mjs @@ -1,13 +1,16 @@ +import Returns from '../core/Returns.mjs' export * from './Types/generic.mjs' export const reducingOperation = { - 'undefined': () => u => u, - 'undefined,...any': () => (u, rest) => u, - 'any,undefined': () => (x, u) => u, - 'undefined,undefined': () => (u,v) => u, - any: () => x => x, + 'undefined': () => Returns('undefined', u => u), + 'undefined,...any': () => Returns('undefined', (u, rest) => u), + 'any,undefined': () => Returns('undefined', (x, u) => u), + 'undefined,undefined': () => Returns('undefined', (u,v) => u), + T: ({T}) => Returns(T, x => x), + // Unfortunately the type language of Pocomath is not (yet?) expressive + // enough to properly type the full reduction signature here: 'any,any,...any': ({ self - }) => (a,b,rest) => [b, ...rest].reduce((x,y) => self(x,y), a) + }) => Returns('any', (a,b,rest) => [b, ...rest].reduce((x,y) => self(x,y), a)) } diff --git a/src/generic/relational.mjs b/src/generic/relational.mjs index a1639a1..72de34c 100644 --- a/src/generic/relational.mjs +++ b/src/generic/relational.mjs @@ -1,34 +1,45 @@ +import Returns from '../core/Returns.mjs' + export const compare = { - 'undefined,undefined': () => () => 0 + 'undefined,undefined': () => Returns('NumInt', () => 0) } export const isZero = { - 'undefined': () => u => u === 0, - T: ({'equal(T,T)': eq, 'zero(T)': zr}) => t => eq(t, zr(t)) + 'undefined': () => Returns('boolean', u => u === 0), + T: ({ + T, + 'equal(T,T)': eq, + 'zero(T)': zr + }) => Returns('boolean', t => eq(t, zr(t))) } export const equal = { - 'any,any': ({equalTT, joinTypes, Templates, typeOf}) => (x,y) => { + 'any,any': ({ + equalTT, + joinTypes, + Templates, + typeOf + }) => Returns('boolean', (x,y) => { const resultant = joinTypes([typeOf(x), typeOf(y)], 'convert') if (resultant === 'any' || resultant in Templates) { return false } return equalTT(x,y) - } + }) } export const equalTT = { 'T,T': ({ 'compare(T,T)': cmp, 'isZero(T)': isZ - }) => (x,y) => isZ(cmp(x,y)), + }) => Returns('boolean', (x,y) => isZ(cmp(x,y))) // If templates were native to typed-function, we should be able to // do something like: // 'any,any': () => () => false // should only be hit for different types } export const unequal = { - 'any,any': ({equal}) => (x,y) => !(equal(x,y)) + 'any,any': ({equal}) => Returns('boolean', (x,y) => !(equal(x,y))) } export const larger = { @@ -36,7 +47,7 @@ export const larger = { 'compare(T,T)': cmp, 'one(T)' : uno, 'equalTT(T,T)' : eq - }) => (x,y) => eq(cmp(x,y), uno(y)) + }) => Returns('boolean', (x,y) => eq(cmp(x,y), uno(y))) } export const largerEq = { @@ -45,10 +56,10 @@ export const largerEq = { 'one(T)' : uno, 'isZero(T)' : isZ, 'equalTT(T,T)': eq - }) => (x,y) => { + }) => Returns('boolean', (x,y) => { const c = cmp(x,y) return isZ(c) || eq(c, uno(y)) - } + }) } export const smaller = { @@ -57,10 +68,10 @@ export const smaller = { 'one(T)' : uno, 'isZero(T)' : isZ, unequal - }) => (x,y) => { + }) => Returns('boolean', (x,y) => { const c = cmp(x,y) return !isZ(c) && unequal(c, uno(y)) - } + }) } export const smallerEq = { @@ -68,5 +79,5 @@ export const smallerEq = { 'compare(T,T)': cmp, 'one(T)' : uno, unequal - }) => (x,y) => unequal(cmp(x,y), uno(y)) + }) => Returns('boolean', (x,y) => unequal(cmp(x,y), uno(y))) } diff --git a/src/generic/roundquotient.mjs b/src/generic/roundquotient.mjs index 5346882..9c2ba2b 100644 --- a/src/generic/roundquotient.mjs +++ b/src/generic/roundquotient.mjs @@ -1,3 +1,9 @@ +import Returns from '../core/Returns.mjs' + export const roundquotient = { - 'T,T': ({'round(T)': rnd, 'divide(T,T)':div}) => (n,d) => rnd(div(n,d)) + 'T,T': ({ + T, + 'round(T)': rnd, + 'divide(T,T)':div + }) => Returns(T, (n,d) => rnd(div(n,d))) } diff --git a/src/generic/sign.mjs b/src/generic/sign.mjs index 769e2c9..cec73cd 100644 --- a/src/generic/sign.mjs +++ b/src/generic/sign.mjs @@ -1,3 +1,9 @@ +import Returns from '../core/Returns.mjs' + export const sign = { - T: ({'compare(T,T)': cmp, 'zero(T)': Z}) => x => cmp(x, Z(x)) + T: ({ + T, + 'compare(T,T)': cmp, + 'zero(T)': Z + }) => Returns(T, x => cmp(x, Z(x))) } diff --git a/src/generic/sqrt.mjs b/src/generic/sqrt.mjs index 21aa1d5..faea759 100644 --- a/src/generic/sqrt.mjs +++ b/src/generic/sqrt.mjs @@ -1,3 +1,4 @@ +import Returns from '../core/Returns.mjs' export * from './Types/generic.mjs' -export const sqrt = {undefined: () => () => undefined} +export const sqrt = {undefined: () => Returns('undefined', () => undefined)} diff --git a/src/generic/square.mjs b/src/generic/square.mjs index 53fd6c2..2619c29 100644 --- a/src/generic/square.mjs +++ b/src/generic/square.mjs @@ -1,3 +1,6 @@ +import {Returns, returnTypeOf} from '../core/Returns.mjs' + export const square = { - T: ({'multiply(T,T)': multT}) => x => multT(x,x) + T: ({'multiply(T,T)': multT}) => Returns( + returnTypeOf(multT), x => multT(x,x)) } diff --git a/src/generic/subtract.mjs b/src/generic/subtract.mjs index b048d0c..35dab22 100644 --- a/src/generic/subtract.mjs +++ b/src/generic/subtract.mjs @@ -1,3 +1,9 @@ +import Returns from '../core/Returns.mjs' + export const subtract = { - 'T,T': ({'add(T,T)': addT, 'negate(T)': negT}) => (x,y) => addT(x, negT(y)) + 'T,T': ({ + T, + 'add(T,T)': addT, + 'negate(T)': negT + }) => Returns(T, (x,y) => addT(x, negT(y))) } diff --git a/src/number/abs.mjs b/src/number/abs.mjs index 66ede16..80b45d8 100644 --- a/src/number/abs.mjs +++ b/src/number/abs.mjs @@ -1,3 +1,4 @@ +import Returns from '../core/Returns.mjs' export * from './Types/number.mjs' -export const abs = {number: () => n => Math.abs(n)} +export const abs = {'T:number': ({T}) => Returns(T, n => Math.abs(n))} diff --git a/src/number/absquare.mjs b/src/number/absquare.mjs index d6ab55a..31a417d 100644 --- a/src/number/absquare.mjs +++ b/src/number/absquare.mjs @@ -1,6 +1,7 @@ +import Returns from '../core/Returns.mjs' export * from './Types/number.mjs' /* Absolute value squared */ export const absquare = { - number: ({'square(number)': sqn}) => n => sqn(n) + 'T:number': ({T, 'square(T)': sqn}) => Returns(T, n => sqn(n)) } diff --git a/src/number/add.mjs b/src/number/add.mjs index 7d79637..41791cf 100644 --- a/src/number/add.mjs +++ b/src/number/add.mjs @@ -1,3 +1,8 @@ +import Returns from '../core/Returns.mjs' export * from './Types/number.mjs' -export const add = {'number,number': () => (m,n) => m+n} +export const add = { + // Note the below assumes that all subtypes of number that will be defined + // are closed under addition! + 'T:number, T': ({T}) => Returns(T, (m,n) => m+n) +} diff --git a/src/number/compare.mjs b/src/number/compare.mjs index 4dc865b..c4b1c26 100644 --- a/src/number/compare.mjs +++ b/src/number/compare.mjs @@ -1,3 +1,5 @@ +import Returns from '../core/Returns.mjs' + /* Lifted from mathjs/src/utils/number.js */ /** * Minimum number added to one that makes the result different than one @@ -48,5 +50,6 @@ function nearlyEqual (x, y, epsilon) { export const compare = { 'number,number': ({ config - }) => (x,y) => nearlyEqual(x, y, config.epsilon) ? 0 : (x > y ? 1 : -1) + }) => Returns( + 'NumInt', (x,y) => nearlyEqual(x, y, config.epsilon) ? 0 : (x > y ? 1 : -1)) } diff --git a/src/number/invert.mjs b/src/number/invert.mjs index 4eabe2f..780ad72 100644 --- a/src/number/invert.mjs +++ b/src/number/invert.mjs @@ -1,3 +1,5 @@ +import Returns from '../core/Returns.mjs' + export * from './Types/number.mjs' -export const invert = {number: () => n => 1/n} +export const invert = {number: () => Returns('number', n => 1/n)} diff --git a/src/number/isZero.mjs b/src/number/isZero.mjs index c15549e..0209daa 100644 --- a/src/number/isZero.mjs +++ b/src/number/isZero.mjs @@ -1,6 +1,6 @@ +import Returns from '../core/Returns.mjs' export * from './Types/number.mjs' export const isZero = { - number: () => n => n === 0, - NumInt: () => n => n === 0 // necessary because of generic template + 'T:number': () => Returns('boolean', n => n === 0) } diff --git a/src/number/multiply.mjs b/src/number/multiply.mjs index 80573d1..5951f22 100644 --- a/src/number/multiply.mjs +++ b/src/number/multiply.mjs @@ -1,3 +1,5 @@ +import Returns from '../core/Returns.mjs' + export * from './Types/number.mjs' -export const multiply = {'number,number': () => (m,n) => m*n} +export const multiply = {'T:number,T': ({T}) => Returns(T, (m,n) => m*n)} diff --git a/src/number/native.mjs b/src/number/native.mjs index 6746408..fcebecc 100644 --- a/src/number/native.mjs +++ b/src/number/native.mjs @@ -1,5 +1,5 @@ import gcdType from '../generic/gcdType.mjs' -import {identity} from '../generic/identity.mjs' +import {identitySubTypes} from '../generic/identity.mjs' export * from './Types/number.mjs' @@ -7,7 +7,7 @@ export {abs} from './abs.mjs' export {absquare} from './absquare.mjs' export {add} from './add.mjs' export {compare} from './compare.mjs' -export const conjugate = {number: () => identity} +export const conjugate = {'T:number': identitySubTypes('number')} export const gcd = gcdType('NumInt') export {invert} from './invert.mjs' export {isZero} from './isZero.mjs' diff --git a/src/number/negate.mjs b/src/number/negate.mjs index 82e27d0..f2336c7 100644 --- a/src/number/negate.mjs +++ b/src/number/negate.mjs @@ -1,3 +1,6 @@ +import Returns from '../core/Returns.mjs' export * from './Types/number.mjs' -export const negate = {number: () => n => -n} +export const negate = { + 'T:number': ({T}) => Returns(T, n => -n) +} diff --git a/src/number/one.mjs b/src/number/one.mjs index 5726468..e38d0dc 100644 --- a/src/number/one.mjs +++ b/src/number/one.mjs @@ -1,3 +1,5 @@ +import Returns from '../core/Returns.mjs' + export * from './Types/number.mjs' -export const one = {number: () => () => 1} +export const one = {number: () => Returns('NumInt', () => 1)} diff --git a/src/number/quotient.mjs b/src/number/quotient.mjs index e8ed83a..b307709 100644 --- a/src/number/quotient.mjs +++ b/src/number/quotient.mjs @@ -1,15 +1,10 @@ +import Returns from '../core/Returns.mjs' + export * from './Types/number.mjs' -const intquotient = () => (n,d) => { - if (d === 0) return d - return Math.floor(n/d) -} - export const quotient = { - // Hmmm, seem to need all of these because of the generic template version - // Should be a way around that - 'NumInt,NumInt': intquotient, - 'NumInt,number': intquotient, - 'number,NumInt': intquotient, - 'number,number': intquotient + 'T:number,T': () => Returns('NumInt', (n,d) => { + if (d === 0) return d + return Math.floor(n/d) + }) } diff --git a/src/number/roundquotient.mjs b/src/number/roundquotient.mjs index 401d499..8c4c519 100644 --- a/src/number/roundquotient.mjs +++ b/src/number/roundquotient.mjs @@ -1,8 +1,10 @@ +import Returns from '../core/Returns.mjs' + export * from './Types/number.mjs' export const roundquotient = { - 'number,number': () => (n,d) => { + 'number,number': () => Returns('NumInt', (n,d) => { if (d === 0) return d return Math.round(n/d) - } + }) } diff --git a/src/number/sqrt.mjs b/src/number/sqrt.mjs index 3017e82..2e5e734 100644 --- a/src/number/sqrt.mjs +++ b/src/number/sqrt.mjs @@ -1,3 +1,4 @@ +import Returns from '../core/Returns.mjs' export * from './Types/number.mjs' export const sqrt = { @@ -5,13 +6,13 @@ export const sqrt = { config, 'complex(number,number)': cplx, 'negate(number)': neg}) => { - if (config.predictable || !cplx) { - return n => isNaN(n) ? NaN : Math.sqrt(n) + if (config.predictable || !cplx) { + return Returns('number', n => isNaN(n) ? NaN : Math.sqrt(n)) + } + return Returns('number|Complex', n => { + if (isNaN(n)) return NaN + if (n >= 0) return Math.sqrt(n) + return cplx(0, Math.sqrt(neg(n))) + }) } - return n => { - if (isNaN(n)) return NaN - if (n >= 0) return Math.sqrt(n) - return cplx(0, Math.sqrt(neg(n))) - } - } } diff --git a/src/number/zero.mjs b/src/number/zero.mjs index 40ac2fb..5e3e3a3 100644 --- a/src/number/zero.mjs +++ b/src/number/zero.mjs @@ -1,3 +1,4 @@ +import Returns from '../core/Returns.mjs' export * from './Types/number.mjs' -export const zero = {number: () => () => 0} +export const zero = {number: () => Returns('NumInt', () => 0)} diff --git a/src/ops/choose.mjs b/src/ops/choose.mjs index c285dc7..dd22dc2 100644 --- a/src/ops/choose.mjs +++ b/src/ops/choose.mjs @@ -1,11 +1,13 @@ -/* Note this is not a good algorithm for computing binomial coefficients, +import Returns from '../core/Returns.mjs' + +/* Note this is _not_ a good algorithm for computing binomial coefficients, * it's just for demonstration purposes */ export const choose = { - 'NumInt,NumInt': ({factorial}) => (n,k) => Number( - factorial(n) / (factorial(k)*factorial(n-k))), + 'NumInt,NumInt': ({factorial}) => Returns( + 'NumInt', (n,k) => Number(factorial(n) / (factorial(k)*factorial(n-k)))), 'bigint,bigint': ({ factorial - }) => (n,k) => factorial(n) / (factorial(k)*factorial(n-k)) + }) => Returns('bigint', (n,k) => factorial(n) / (factorial(k)*factorial(n-k))) } diff --git a/src/ops/factorial.mjs b/src/ops/factorial.mjs index bb07047..b1154f3 100644 --- a/src/ops/factorial.mjs +++ b/src/ops/factorial.mjs @@ -1,8 +1,15 @@ -export function factorial(n) { +import Returns from '../core/Returns.mjs' + +/* Plain functions are OK, too, and they can be decorated with a return type + * just like an implementation. + */ +const factorial = Returns('bigint', function factorial(n) { n = BigInt(n) let prod = 1n for (let i = n; i > 1n; --i) { prod *= i } return prod -} +}) + +export {factorial} diff --git a/src/ops/floor.mjs b/src/ops/floor.mjs index 5fdb9e7..3754dcb 100644 --- a/src/ops/floor.mjs +++ b/src/ops/floor.mjs @@ -1,3 +1,4 @@ +import Returns from '../core/Returns.mjs' import {Complex} from '../complex/Types/Complex.mjs' /* Note we don't **export** any types here, so that only the options @@ -5,26 +6,28 @@ import {Complex} from '../complex/Types/Complex.mjs' */ export const floor = { - bigint: () => x => x, - NumInt: () => x => x, // Because Pocomath isn't part of typed-function, or - 'Complex': () => x => x, // at least have access to the real - // typed-function parse, we unfortunately can't coalesce these into one - // entry with type `bigint|NumInt|GaussianInteger` because they couldn't - // be separately activated then + /* Because Pocomath isn't part of typed-function, nor does it have access + * to the real typed-function parse, we unfortunately can't coalesce the + * first several implementations into one entry with type + * `bigint|NumInt|GaussianInteger` because then they couldn't + * be separately activated + */ + bigint: () => Returns('bigint', x => x), + NumInt: () => Returns('NumInt', x => x), + 'Complex': () => Returns('Complex', x => x), - number: ({'equalTT(number,number)': eq}) => n => { + number: ({'equalTT(number,number)': eq}) => Returns('NumInt', n => { if (eq(n, Math.round(n))) return Math.round(n) return Math.floor(n) - }, + }), 'Complex': Complex.promoteUnary['Complex'], // OK to include a type totally not in Pocomath yet, it'll never be // activated. - // Fraction: ({quotient}) => f => quotient(f.n, f.d), // oops have that now BigNumber: ({ 'round(BigNumber)': rnd, 'equal(BigNumber,BigNumber)': eq - }) => x => eq(x,round(x)) ? round(x) : x.floor() + }) => Returns('BigNumber', x => eq(x,round(x)) ? round(x) : x.floor()) } diff --git a/src/tuple/Types/Tuple.mjs b/src/tuple/Types/Tuple.mjs index d76d147..4ff6685 100644 --- a/src/tuple/Types/Tuple.mjs +++ b/src/tuple/Types/Tuple.mjs @@ -1,25 +1,24 @@ /* A template type representing a homeogeneous tuple of elements */ import PocomathInstance from '../../core/PocomathInstance.mjs' +import {Returns, returnTypeOf} from '../../core/Returns.mjs' const Tuple = new PocomathInstance('Tuple') -// First a base type that will generally not be used directly -Tuple.installType('Tuple', { - test: t => t && typeof t === 'object' && 'elts' in t && Array.isArray(t.elts) -}) -// Now the template type that is the primary use of this + Tuple.installType('Tuple', { - // We are assuming that any 'Type' refines 'Type', so this is - // not necessary: - // refines: 'Tuple', - // But we need there to be a way to determine the type of a tuple: - infer: ({typeOf, joinTypes}) => t => joinTypes(t.elts.map(typeOf)), - // For the test, we can assume that t is already a base tuple, - // and we get the test for T as an input and we have to return - // the test for Tuple + // A test that "defines" the "base type", which is not really a type + // (only fully instantiated types are added to the universe) + base: t => t && typeof t === 'object' && 'elts' in t && Array.isArray(t.elts), + // The template portion of the test; it takes the test for T as + // input and returns the test for an entity _that already passes + // the base test_ to be a Tuple: test: testT => t => t.elts.every(testT), - // These are only invoked for types U such that there is already - // a conversion from U to T, and that conversion is passed as an input - // and we have to return the conversion to Tuple: + // And we need there to be a way to determine the (instantiation) + // type of an tuple (that has already passed the base test): + infer: ({typeOf, joinTypes}) => t => joinTypes(t.elts.map(typeOf)), + // Conversions. Parametrized conversions are only invoked for types + // U such that there is already a conversion from U to T, and that + // conversion is passed as an input, and we have to return the conversion + // function from the indicated template in terms of U to Tuple: from: { 'Tuple': convert => tu => ({elts: tu.elts.map(convert)}), // Here since there is no U it's a straight conversion: @@ -35,50 +34,66 @@ Tuple.promoteUnary = { 'Tuple': ({ 'self(T)': me, tuple - }) => t => tuple(...(t.elts.map(x => me(x)))) // NOTE: this must use - // the inner arrow function to drop additional arguments that Array.map - // supplies, as otherwise the wrong signature of `me` might be used. + }) => { + const compType = me.fromInstance.joinTypes( + returnTypeOf(me).split('|'), 'convert') + return Returns( + `Tuple<${compType}>`, t => tuple(...(t.elts.map(x => me(x))))) + } } Tuple.promoteBinaryUnary = { - 'Tuple,Tuple': ({'self(T,T)': meB, 'self(T)': meU, tuple}) => (s,t) => { - let i = -1 - let result = [] - while (true) { - i += 1 - if (i < s.elts.length) { - if (i < t.elts.length) result.push(meB(s.elts[i], t.elts[i])) - else result.push(meU(s.elts[i])) - continue + 'Tuple,Tuple': ({'self(T,T)': meB, 'self(T)': meU, tuple}) => { + const compTypes = returnTypeOf(meB).split('|').concat( + returnTypeOf(meU).split('|')) + const compType = meU.fromInstance.joinTypes(compTypes, 'convert') + return Returns(`Tuple<${compType}>`, (s,t) => { + let i = -1 + let result = [] + while (true) { + i += 1 + if (i < s.elts.length) { + if (i < t.elts.length) result.push(meB(s.elts[i], t.elts[i])) + else result.push(meU(s.elts[i])) + continue + } + if (i < t.elts.length) result.push(meU(t.elts[i])) + else break } - if (i < t.elts.length) result.push(meU(t.elts[i])) - else break - } - return tuple(...result) + return tuple(...result) + }) } } Tuple.promoteBinary = { - 'Tuple,Tuple': ({'self(T,T)': meB, tuple}) => (s,t) => { - const lim = Math.max(s.elts.length, t.elts.length) - const result = [] - for (let i = 0; i < lim; ++i) { - result.push(meB(s.elts[i], t.elts[i])) - } - return tuple(...result) + 'Tuple,Tuple': ({'self(T,T)': meB, tuple}) => { + const compType = meB.fromInstance.joinTypes( + returnTypeOf(meB).split('|')) + return Returns(`Tuple<${compType}>`, (s,t) => { + const lim = Math.max(s.elts.length, t.elts.length) + const result = [] + for (let i = 0; i < lim; ++i) { + result.push(meB(s.elts[i], t.elts[i])) + } + return tuple(...result) + }) } } Tuple.promoteBinaryStrict = { - 'Tuple,Tuple': ({'self(T,T)': meB, tuple}) => (s,t) => { - if (s.elts.length !== t.elts.length) { - throw new RangeError('Tuple length mismatch') // get name of self ?? - } - const result = [] - for (let i = 0; i < s.elts.length; ++i) { - result.push(meB(s.elts[i], t.elts[i])) - } - return tuple(...result) + 'Tuple,Tuple': ({'self(T,T)': meB, tuple}) => { + const compType = meB.fromInstance.joinTypes( + returnTypeOf(meB).split('|')) + return Returns(`Tuple<${compType}>`, (s,t) => { + if (s.elts.length !== t.elts.length) { + throw new RangeError('Tuple length mismatch') // get name of self ?? + } + const result = [] + for (let i = 0; i < s.elts.length; ++i) { + result.push(meB(s.elts[i], t.elts[i])) + } + return tuple(...result) + }) } } diff --git a/src/tuple/equalTT.mjs b/src/tuple/equalTT.mjs index 1606410..557ee2d 100644 --- a/src/tuple/equalTT.mjs +++ b/src/tuple/equalTT.mjs @@ -1,11 +1,16 @@ +import Returns from '../core/Returns.mjs' + export * from './Types/Tuple.mjs' export const equalTT = { - 'Tuple,Tuple': ({'self(T,T)': me, 'length(Tuple)': len}) => (s,t) => { + 'Tuple,Tuple': ({ + 'self(T,T)': me, + 'length(Tuple)': len + }) => Returns('boolean', (s,t) => { if (len(s) !== len(t)) return false for (let i = 0; i < len(s); ++i) { if (!me(s.elts[i], t.elts[i])) return false } return true - } + }) } diff --git a/src/tuple/isZero.mjs b/src/tuple/isZero.mjs index 9375277..a48e92a 100644 --- a/src/tuple/isZero.mjs +++ b/src/tuple/isZero.mjs @@ -1,7 +1,10 @@ +import Returns from '../core/Returns.mjs' + export {Tuple} from './Types/Tuple.mjs' export const isZero = { - 'Tuple': ({'self(T)': me}) => t => t.elts.every(e => me(e)) + 'Tuple': ({'self(T)': me}) => Returns( + 'boolean', t => t.elts.every(e => me(e))) // Note we can't just say `every(me)` above since every invokes its // callback with more arguments, which then violates typed-function's // signature for `me` diff --git a/src/tuple/length.mjs b/src/tuple/length.mjs index f3e8f2d..4df2c74 100644 --- a/src/tuple/length.mjs +++ b/src/tuple/length.mjs @@ -1,3 +1,4 @@ +import Returns from '../core/Returns.mjs' export {Tuple} from './Types/Tuple.mjs' -export const length = {Tuple: () => t => t.elts.length} +export const length = {'Tuple': () => Returns('NumInt', t => t.elts.length)} diff --git a/src/tuple/tuple.mjs b/src/tuple/tuple.mjs index 893b54d..9cd0c65 100644 --- a/src/tuple/tuple.mjs +++ b/src/tuple/tuple.mjs @@ -1,6 +1,10 @@ +import Returns from '../core/Returns.mjs' + export {Tuple} from './Types/Tuple.mjs' /* The purpose of the template argument is to ensure that all of the args * are convertible to the same type. */ -export const tuple = {'...T': () => args => ({elts: args})} +export const tuple = { + '...T': ({T}) => Returns(`Tuple<${T}>`, args => ({elts: args})) +} diff --git a/test/_pocomath.mjs b/test/_pocomath.mjs index 38df18b..c4a5c28 100644 --- a/test/_pocomath.mjs +++ b/test/_pocomath.mjs @@ -20,9 +20,56 @@ describe('The default full pocomath instance "math"', () => { assert.strictEqual(math.typeOf({re: 6.28, im: 2.72}), 'Complex') }) + it('can determine the return types of operations', () => { + assert.strictEqual(math.returnTypeOf('negate', 'number'), 'number') + assert.strictEqual(math.returnTypeOf('negate', 'NumInt'), 'NumInt') + math.negate(math.complex(1.2, 2.8)) // TODO: make this call unnecessary + assert.strictEqual( + math.returnTypeOf('negate', 'Complex'), 'Complex') + assert.strictEqual(math.returnTypeOf('add', 'number,number'), 'number') + assert.strictEqual(math.returnTypeOf('add', 'NumInt,NumInt'), 'NumInt') + assert.strictEqual(math.returnTypeOf('add', 'NumInt,number'), 'number') + assert.strictEqual(math.returnTypeOf('add', 'number,NumInt'), 'number') + assert.deepStrictEqual( // TODO: ditto + math.add(3, math.complex(2.5, 1)), math.complex(5.5, 1)) + assert.strictEqual( + math.returnTypeOf('add', 'Complex,NumInt'), 'Complex') + // The following is not actually what we want, but the Pocomath type + // language isn't powerful enough at this point to capture the true + // return type: + assert.strictEqual( + math.returnTypeOf('add', 'number,NumInt,Complex'), 'any') + assert.strictEqual( + math.returnTypeOf('chain', 'bigint'), 'Chain') + assert.strictEqual( + math.returnTypeOf('returnTypeOf', 'string,string'), 'string') + assert.strictEqual( + math.returnTypeOf('conjugate', 'bigint'), 'bigint') + assert.strictEqual( + math.returnTypeOf('gcd', 'bigint,bigint'), 'bigint') + math.identity(math.fraction(3,5)) // TODO: ditto + assert.strictEqual(math.returnTypeOf('identity', 'Fraction'), 'Fraction') + assert.strictEqual( + math.returnTypeOf('quotient', 'bigint,bigint'), 'bigint') + math.abs(math.complex(2,1)) //TODO: ditto + assert.strictEqual( + math.returnTypeOf('abs','Complex'), 'number') + math.multiply(math.quaternion(1,1,1,1), math.quaternion(1,-1,1,-1)) // dit + const quatType = math.returnTypeOf( + 'quaternion', 'NumInt,NumInt,NumInt,NumInt') + assert.strictEqual(quatType, 'Complex>') + assert.strictEqual( + math.returnTypeOf('multiply', quatType + ',' + quatType), quatType) + assert.strictEqual(math.returnTypeOf('isZero', 'NumInt'), 'boolean') + assert.strictEqual( + math.returnTypeOf('roundquotient', 'NumInt,number'), 'NumInt') + assert.strictEqual( + math.returnTypeOf('factorial', 'NumInt'), 'bigint') + }) + it('can subtract numbers', () => { assert.strictEqual(math.subtract(12, 5), 7) - //assert.strictEqual(math.subtract(3n, 1.5), 1.5) + assert.throws(() => math.subtract(3n, 1.5), 'TypeError') }) it('can add numbers', () => { diff --git a/test/complex/_all.mjs b/test/complex/_all.mjs index 531a28d..413503c 100644 --- a/test/complex/_all.mjs +++ b/test/complex/_all.mjs @@ -39,8 +39,8 @@ describe('complex', () => { }) 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(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))) }) @@ -83,6 +83,7 @@ describe('complex', () => { assert.deepStrictEqual( math.multiply(q0, math.quaternion(2, 1, 0.1, 0.1)), math.quaternion(1.9, 1.1, 2.1, -0.9)) + math.absquare(math.complex(1.25, 2.5)) //HACK: need absquare(Complex) assert.strictEqual(math.abs(q0), Math.sqrt(2)) assert.strictEqual(math.abs(q1), Math.sqrt(33)/4) }) diff --git a/test/core/_utils.mjs b/test/core/_utils.mjs new file mode 100644 index 0000000..3976517 --- /dev/null +++ b/test/core/_utils.mjs @@ -0,0 +1,8 @@ +import assert from 'assert' +import * as utils from '../../src/core/utils.mjs' + +describe('typeListOfSignature', () => { + it('returns an empty list for the empty signature', () => { + assert.deepStrictEqual(utils.typeListOfSignature(''), []) + }) +}) diff --git a/test/custom.mjs b/test/custom.mjs index f6612ef..b6fd111 100644 --- a/test/custom.mjs +++ b/test/custom.mjs @@ -135,13 +135,8 @@ describe('A custom instance', () => { assert.strictEqual( inst.typeMerge(3, inst.complex(4.5,2.1)), 'Merge to Complex') - // The following is the current behavior, since 3 converts to 3+0i - // which is technically the same Complex type as 3n+0ni. - // This should clear up when Complex is templatized - assert.strictEqual(inst.typeMerge(3, inst.complex(3n)), 'Merge to Complex') - // But types that truly cannot be merged should throw a TypeError - // Should add a variation of this with a more usual type once there is - // one not interconvertible with others... + assert.throws( + () => inst.typeMerge(3, inst.complex(3n)), TypeError) inst.install(genericSubtract) assert.throws(() => inst.typeMerge(3, undefined), TypeError) }) diff --git a/test/generic/_all.mjs b/test/generic/_all.mjs new file mode 100644 index 0000000..94829a4 --- /dev/null +++ b/test/generic/_all.mjs @@ -0,0 +1,20 @@ +import assert from 'assert' +import math from '../../src/pocomath.mjs' + +describe('generic', () => { + it('calculates mean', () => { + assert.strictEqual(math.mean(1,2.5,3.25,4.75), 2.875) + assert.strictEqual( + math.returnTypeOf('mean', 'number,number,number,number'), + 'number' + ) + }) + it('compares things', () => { + assert.strictEqual(math.larger(7n, 3n), true) + assert.strictEqual( + math.returnTypeOf('larger', 'bigint,bigint'), 'boolean') + assert.strictEqual(math.smallerEq(7.2, 3), false) + assert.strictEqual( + math.returnTypeOf('smallerEq', 'number,NumInt'), 'boolean') + }) +}) diff --git a/test/generic/fraction.mjs b/test/generic/fraction.mjs index 9af3b8d..f1fcd97 100644 --- a/test/generic/fraction.mjs +++ b/test/generic/fraction.mjs @@ -92,4 +92,10 @@ describe('fraction', () => { assert.deepStrictEqual(math.square(tf), math.fraction(9/16)) }) + it('knows the types of its operations', () => { + assert.deepStrictEqual( + math.returnTypeOf('ceiling', 'Fraction'), 'Fraction') + assert.deepStrictEqual( + math.returnTypeOf('multiply', 'Fraction,Fraction'), 'Fraction') + }) }) diff --git a/test/tuple/_native.mjs b/test/tuple/_native.mjs index 2cf56d1..8a9b34c 100644 --- a/test/tuple/_native.mjs +++ b/test/tuple/_native.mjs @@ -8,14 +8,12 @@ describe('tuple', () => { it('does not allow unification by converting consecutive arguments', () => { assert.throws(() => math.tuple(3, 5.2, 2n), /TypeError.*unif/) - // Hence, the order matters in a slightly unfortunate way, - // but I think being a little ragged in these edge cases is OK: assert.throws( () => math.tuple(3, 2n, math.complex(5.2)), /TypeError.*unif/) - assert.deepStrictEqual( - math.tuple(3, math.complex(2n), 5.2), - {elts: [math.complex(3), math.complex(2n), math.complex(5.2)]}) + assert.throws( + () => math.tuple(3, math.complex(2n), 5.2), + /TypeError.*unif/) }) it('can be tested for zero and equality', () => { @@ -56,6 +54,9 @@ describe('tuple', () => { assert.deepStrictEqual( math.subtract(math.tuple(3n,4n,5n), math.tuple(2n,1n,0n)), math.tuple(1n,3n,5n)) + assert.deepStrictEqual( + math.returnTypeOf('subtract', 'Tuple,Tuple'), + 'Tuple') assert.throws( () => math.subtract(math.tuple(5,6), math.tuple(7)), /RangeError/) @@ -106,9 +107,16 @@ describe('tuple', () => { }) it('supports sqrt', () => { + const mixedTuple = math.tuple(2, math.complex(0,2), 1.5) assert.deepStrictEqual( - math.sqrt(math.tuple(4,-4,2.25)), - math.tuple(2, math.complex(0,2), 1.5)) + mixedTuple, + math.tuple(math.complex(2), math.complex(0,2), math.complex(1.5))) + assert.strictEqual( + math.returnTypeOf('tuple', 'NumInt, Complex, number'), + 'Tuple>') + assert.deepStrictEqual(math.sqrt(math.tuple(4,-4,2.25)), mixedTuple) + assert.strictEqual( + math.returnTypeOf('sqrt', 'Tuple'), 'Tuple>') }) }) From 0dbb95bbbe916d7928bedf5669a81cfeb2d3906b Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Thu, 1 Dec 2022 17:47:20 +0000 Subject: [PATCH 22/22] feat(polynomialRoot) (#57) Implements a simply polynomial root finder function polynomialRoot, intended to be used for benchmarking against mathjs. For this purpose, adds numerous other functions (e.g. cbrt, arg, cis), refactors sqrt (so that you can definitely get the complex square root when you want it), and makes numerous enhancements to the core so that a template can match after conversions. Co-authored-by: Glen Whitney Reviewed-on: https://code.studioinfinity.org/glen/pocomath/pulls/57 --- src/complex/abs.mjs | 1 - src/complex/arg.mjs | 7 + src/complex/cbrtc.mjs | 28 ++ src/complex/cis.mjs | 9 + src/complex/isReal.mjs | 7 + src/complex/native.mjs | 6 + src/complex/polynomialRoot.mjs | 118 ++++++++ src/complex/sqrt.mjs | 51 +--- src/complex/sqrtc.mjs | 41 +++ src/core/PocomathInstance.mjs | 490 +++++++++++++++++-------------- src/core/extractors.mjs | 2 +- src/number/add.mjs | 2 +- src/number/cbrt.mjs | 19 ++ src/number/native.mjs | 1 + src/tuple/tuple.mjs | 2 +- test/complex/_all.mjs | 5 + test/complex/_polynomialRoot.mjs | 63 ++++ tools/approx.mjs | 46 +++ 18 files changed, 634 insertions(+), 264 deletions(-) create mode 100644 src/complex/arg.mjs create mode 100644 src/complex/cbrtc.mjs create mode 100644 src/complex/cis.mjs create mode 100644 src/complex/isReal.mjs create mode 100644 src/complex/polynomialRoot.mjs create mode 100644 src/complex/sqrtc.mjs create mode 100644 src/number/cbrt.mjs create mode 100644 test/complex/_polynomialRoot.mjs create mode 100644 tools/approx.mjs diff --git a/src/complex/abs.mjs b/src/complex/abs.mjs index 53a2464..3ea0274 100644 --- a/src/complex/abs.mjs +++ b/src/complex/abs.mjs @@ -3,7 +3,6 @@ export * from './Types/Complex.mjs' export const abs = { 'Complex': ({ - T, sqrt, // Unfortunately no notation yet for the needed signature 'absquare(T)': baseabsq, 'absquare(Complex)': absq diff --git a/src/complex/arg.mjs b/src/complex/arg.mjs new file mode 100644 index 0000000..d654795 --- /dev/null +++ b/src/complex/arg.mjs @@ -0,0 +1,7 @@ +import {Returns, returnTypeOf} from '../core/Returns.mjs' +export * from './Types/Complex.mjs' + +/* arg is the "argument" or angle theta of z in its form r cis theta */ +export const arg = { + 'Complex': () => Returns('number', z => Math.atan2(z.im, z.re)) +} diff --git a/src/complex/cbrtc.mjs b/src/complex/cbrtc.mjs new file mode 100644 index 0000000..118da60 --- /dev/null +++ b/src/complex/cbrtc.mjs @@ -0,0 +1,28 @@ +import {Returns, returnTypeOf} from '../core/Returns.mjs' +export * from './Types/Complex.mjs' + +const TAU3 = 2 * Math.PI / 3 + +/* Complex cube root that returns all three roots as a tuple of complex. */ +/* follows the implementation in mathjs */ +/* Really only works for T = number at the moment because of arg and cbrt */ +export const cbrtc = { + 'Complex': ({ + 'arg(T)': theta, + 'divide(T,T)': div, + 'abs(Complex)': absval, + 'complex(T)': cplx, + 'cbrt(T)': cbrtT, + 'multiply(Complex,Complex)': mult, + 'cis(T)': cisT, + 'tuple(...Complex)': tup + }) => Returns('Tuple>', z => { + const arg3 = div(theta(z), 3) + const r = cplx(cbrtT(absval(z))) + return tup( + mult(r, cisT(arg3)), + mult(r, cisT(arg3 + TAU3)), + mult(r, cisT(arg3 - TAU3)) + ) + }) +} diff --git a/src/complex/cis.mjs b/src/complex/cis.mjs new file mode 100644 index 0000000..fd541e7 --- /dev/null +++ b/src/complex/cis.mjs @@ -0,0 +1,9 @@ +import {Returns, returnTypeOf} from '../core/Returns.mjs' +export * from './Types/Complex.mjs' + +/* Returns cosine plus i sin theta */ +export const cis = { + 'number': ({'complex(number,number)': cplx}) => Returns( + 'Complex', t => cplx(Math.cos(t), Math.sin(t)) + ) +} diff --git a/src/complex/isReal.mjs b/src/complex/isReal.mjs new file mode 100644 index 0000000..57eb7ee --- /dev/null +++ b/src/complex/isReal.mjs @@ -0,0 +1,7 @@ +import Returns from '../core/Returns.mjs' +export * from './Types/Complex.mjs' + +export const isReal = { + 'Complex': ({'equal(T,T)': eq, 'add(T,T)': plus}) => Returns( + 'boolean', z => eq(z.re, plus(z.re, z.im))) +} diff --git a/src/complex/native.mjs b/src/complex/native.mjs index 93c26e4..eba3859 100644 --- a/src/complex/native.mjs +++ b/src/complex/native.mjs @@ -3,18 +3,24 @@ export * from './Types/Complex.mjs' export {abs} from './abs.mjs' export {absquare} from './absquare.mjs' export {add} from './add.mjs' +export {arg} from './arg.mjs' export {associate} from './associate.mjs' +export {cbrtc} from './cbrtc.mjs' +export {cis} from './cis.mjs' export {complex} from './complex.mjs' export {conjugate} from './conjugate.mjs' export {equalTT} from './equalTT.mjs' export {gcd} from './gcd.mjs' export {invert} from './invert.mjs' +export {isReal} from './isReal.mjs' export {isZero} from './isZero.mjs' export {multiply} from './multiply.mjs' export {negate} from './negate.mjs' +export {polynomialRoot} from './polynomialRoot.mjs' export {quaternion} from './quaternion.mjs' export {quotient} from './quotient.mjs' export {roundquotient} from './roundquotient.mjs' export {sqrt} from './sqrt.mjs' +export {sqrtc} from './sqrtc.mjs' export {zero} from './zero.mjs' diff --git a/src/complex/polynomialRoot.mjs b/src/complex/polynomialRoot.mjs new file mode 100644 index 0000000..7a6b9a3 --- /dev/null +++ b/src/complex/polynomialRoot.mjs @@ -0,0 +1,118 @@ +import Returns from '../core/Returns.mjs' +export * from './Types/Complex.mjs' + +export const polynomialRoot = { + 'Complex,...Complex': ({ + T, + 'tuple(...Complex)': tupCplx, + 'tuple(...T)': tupReal, + 'isZero(Complex)': zero, + 'complex(T)': C, + 'multiply(Complex,Complex)': mul, + 'divide(Complex,Complex)': div, + 'negate(Complex)': neg, + 'isReal(Complex)': real, + 'equalTT(Complex,Complex)': eq, + 'add(Complex,Complex)': plus, + 'subtract(Complex,Complex)': sub, + 'sqrtc(Complex)': sqt, + 'cbrtc(Complex)': cbt + }) => Returns(`Tuple<${T}>|Tuple>`, (constant, rest) => { + // helper to convert results to appropriate tuple type + const typedTup = arr => { + if (arr.every(real)) { + return tupReal.apply(tupReal, arr.map(z => z.re)) + } + return tupCplx.apply(tupCplx, arr) + } + + const coeffs = [constant, ...rest] + while (coeffs.length > 0 && zero(coeffs[coeffs.length - 1])) { + coeffs.pop() + } + if (coeffs.length < 2) { + } + switch (coeffs.length) { + case 0: case 1: + throw new RangeError( + `Polynomial [${constant}, ${rest}] must have at least one` + + 'non-zero non-constant coefficient') + case 2: // linear + return typedTup([neg(div(coeffs[0], coeffs[1]))]) + case 3: { // quadratic + const [c, b, a] = coeffs + const denom = mul(C(2), a) + const d1 = mul(b, b) + const d2 = mul(C(4), mul(a, c)) + if (eq(d1, d2)) { + return typedTup([div(neg(b), denom)]) + } + let discriminant = sqt(sub(d1, d2)) + return typedTup([ + div(sub(discriminant, b), denom), + div(sub(neg(discriminant), b), denom) + ]) + } + case 4: { // cubic, cf. https://en.wikipedia.org/wiki/Cubic_equation + const [d, c, b, a] = coeffs + const denom = neg(mul(C(3), a)) + const asqrd = mul(a, a) + const D0_1 = mul(b, b) + const bcubed = mul(D0_1, b) + const D0_2 = mul(C(3), mul(a, c)) + const D1_1 = plus( + mul(C(2), bcubed), mul(C(27), mul(asqrd, d))) + const abc = mul(a, mul(b, c)) + const D1_2 = mul(C(9), abc) + // Check for a triple root + if (eq(D0_1, D0_2) && eq(D1_1, D1_2)) { + return typedTup([div(b, denom)]) + } + const Delta0 = sub(D0_1, D0_2) + const Delta1 = sub(D1_1, D1_2) + const csqrd = mul(c, c) + const discriminant1 = plus( + mul(C(18), mul(abc, d)), mul(D0_1, csqrd)) + const discriminant2 = plus( + mul(C(4), mul(bcubed, d)), + plus( + mul(C(4), mul(a, mul(csqrd, c))), + mul(C(27), mul(asqrd, mul(d, d))))) + // See if we have a double root + if (eq(discriminant1, discriminant2)) { + return typedTup([ + div( + sub( + mul(C(4), abc), + plus(mul(C(9), mul(asqrd, d)), bcubed)), + mul(a, Delta0)), // simple root + div( + sub(mul(C(9), mul(a, d)), mul(b, c)), + mul(C(2), Delta0)) // double root + ]) + } + // OK, we have three distinct roots + let Ccubed + if (eq(D0_1, D0_2)) { + Ccubed = Delta1 + } else { + Ccubed = div( + plus( + Delta1, + sqt(sub( + mul(Delta1, Delta1), + mul(C(4), mul(Delta0, mul(Delta0, Delta0))))) + ), + C(2)) + } + const croots = cbt(Ccubed) + return typedTup(cbt(Ccubed).elts.map( + C => div(plus(b, plus(C, div(Delta0, C))), denom))) + } + default: + throw new RangeError( + 'only implemented for cubic or lower-order polynomials, ' + + `not ${JSON.stringify(coeffs)}`) + } + }) +} diff --git a/src/complex/sqrt.mjs b/src/complex/sqrt.mjs index 7f4044b..cee5278 100644 --- a/src/complex/sqrt.mjs +++ b/src/complex/sqrt.mjs @@ -4,49 +4,30 @@ export * from './Types/Complex.mjs' export const sqrt = { 'Complex': ({ config, + 'sqrtc(Complex)': predictableSqrt, 'isZero(T)': isZ, - 'sign(T)': sgn, - 'one(T)': uno, - 'add(T,T)': plus, - 'complex(T)': cplxU, - 'complex(T,T)': cplxB, - 'multiply(T,T)': mult, - 'self(T)': me, - 'divide(T,T)': div, - 'absquare(Complex)': absqC, - 'subtract(T,T)': sub }) => { - let baseReturns = returnTypeOf(me) - if (baseReturns.includes('|')) { - // Bit of a hack, because it is relying on other implementations - // to list the "typical" value of sqrt first - baseReturns = baseReturns.split('|', 1)[0] - } - + if (config.checkingDependency) return undefined + const complexReturns = returnTypeOf(predictableSqrt) + const baseReturns = complexReturns.slice(8, -1); // Complex if (config.predictable) { - return Returns(`Complex<${baseReturns}>`, z => { - const reOne = uno(z.re) - if (isZ(z.im) && sgn(z.re) === reOne) return cplxU(me(z.re)) - const reTwo = plus(reOne, reOne) - const myabs = me(absqC(z)) - return cplxB( - mult(sgn(z.im), me(div(plus(myabs, z.re), reTwo))), - me(div(sub(myabs, z.re), reTwo)) - ) - }) + return Returns(complexReturns, z => predictableSqrt(z)) } return Returns( `Complex<${baseReturns}>|${baseReturns}|undefined`, z => { - const reOne = uno(z.re) - if (isZ(z.im) && sgn(z.re) === reOne) return me(z.re) - const reTwo = plus(reOne, reOne) - const myabs = me(absqC(z)) - const reSqrt = me(div(plus(myabs, z.re), reTwo)) - const imSqrt = me(div(sub(myabs, z.re), reTwo)) - if (reSqrt === undefined || imSqrt === undefined) return undefined - return cplxB(mult(sgn(z.im), reSqrt), imSqrt) + let complexSqrt + try { + complexSqrt = predictableSqrt(z) + } catch (e) { + return undefined + } + if (complexSqrt.re === undefined || complexSqrt.im === undefined) { + return undefined + } + if (isZ(complexSqrt.im)) return complexSqrt.re + return complexSqrt } ) } diff --git a/src/complex/sqrtc.mjs b/src/complex/sqrtc.mjs new file mode 100644 index 0000000..e909ff7 --- /dev/null +++ b/src/complex/sqrtc.mjs @@ -0,0 +1,41 @@ +import {Returns, returnTypeOf} from '../core/Returns.mjs' +export * from './Types/Complex.mjs' + +export const sqrtc = { + 'Complex': ({ + 'isZero(T)': isZ, + 'sign(T)': sgn, + 'one(T)': uno, + 'add(T,T)': plus, + 'complex(T)': cplxU, + 'complex(T,T)': cplxB, + 'multiply(T,T)': mult, + 'sqrt(T)': sqt, + 'divide(T,T)': div, + 'absquare(Complex)': absqC, + 'subtract(T,T)': sub + }) => { + if (isZ.checkingDependency) return undefined + let baseReturns = returnTypeOf(sqt) + if (baseReturns.includes('|')) { + // Bit of a hack, because it is relying on other implementations + // to list the "typical" value of sqrt first + baseReturns = baseReturns.split('|', 1)[0] + } + return Returns(`Complex<${baseReturns}>`, z => { + const reOne = uno(z.re) + if (isZ(z.im) && sgn(z.re) === reOne) return cplxU(sqt(z.re)) + const myabs = sqt(absqC(z)) + const reTwo = plus(reOne, reOne) + const reQuot = div(plus(myabs, z.re), reTwo) + const imQuot = div(sub(myabs, z.re), reTwo) + if (reQuot === undefined || imQuot === undefined) { + throw new TypeError(`Cannot compute sqrt of ${z.re} + {z.im}i`) + } + return cplxB( + mult(sgn(z.im), sqt(div(plus(myabs, z.re), reTwo))), + sqt(div(sub(myabs, z.re), reTwo)) + ) + }) + } +} diff --git a/src/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index 4b46752..cc1e089 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -7,6 +7,12 @@ import {typeListOfSignature, typesOfSignature, subsetOfKeys} from './utils.mjs' const anySpec = {} // fixed dummy specification of 'any' type +/* Like `.some(predicate)` but for collections */ +function exists(collection, predicate) { + for (const item of collection) if (predicate(item)) return true; + return false; +} + /* Template/signature parsing stuff; should probably be moved to a * separate file, but it's a bit interleaved at the moment */ @@ -87,6 +93,7 @@ function substituteInSignature(signature, parameter, type) { return sig.replaceAll(pattern, type) } +const UniversalType = 'ground' // name for a type that matches anything let lastWhatToDo = null // used in an infinite descent check export default class PocomathInstance { @@ -97,6 +104,7 @@ export default class PocomathInstance { static reserved = new Set([ 'chain', 'config', + 'convert', 'importDependencies', 'install', 'installType', @@ -128,16 +136,30 @@ export default class PocomathInstance { // its onMismatch function, below: this._metaTyped = typed.create() this._metaTyped.clear() + this._metaTyped.addTypes([{name: UniversalType, test: () => true}]) + // And these are the meta bindings: (I think we don't need separate // invalidation for them as they are only accessed through a main call.) this._meta = {} // The resulting typed-functions this._metaTFimps = {} // and their implementations const me = this - const myTyped = this._typed this._typed.onMismatch = (name, args, sigs) => { if (me._invalid.has(name)) { + if (this._fixing === name) { + this._fixingCount += 1 + if (this._fixingCount > this._maxDepthSeen + 2) { + throw new ReferenceError( + `Infinite descent rebuilding ${name} on ${args}`) + } + } else { + this._fixingCount = 0 + } // rebuild implementation and try again - return me[name](...args) + const lastFixing = this._fixing + this._fixing = name + const value = me[name](...args) + this._fix = lastFixing + return value } const metaversion = me._meta[name] if (metaversion) { @@ -183,6 +205,8 @@ export default class PocomathInstance { this._plainFunctions = new Set() // the names of the plain functions this._chainRepository = {} // place to store chainified functions this.joinTypes = this.joinTypes.bind(me) + // Provide access to typed function conversion: + this.convert = this._typed.convert.bind(this._typed) } /** @@ -523,6 +547,10 @@ export default class PocomathInstance { } } } + // Need to metafy ground types + if (type === base) { + this._metafy(type) + } // update the typeOf function const imp = {} imp[type] = {uses: new Set(), does: () => Returns('string', () => type)} @@ -640,7 +668,7 @@ export default class PocomathInstance { } // install the "base type" in the meta universe: - let beforeType = 'any' + let beforeType = UniversalType for (const other of spec.before || []) { if (other in this.templates) { beforeType = other @@ -648,6 +676,13 @@ export default class PocomathInstance { } } this._metaTyped.addTypes([{name: base, test: spec.base}], beforeType) + // Add conversions to the base type: + if (spec.from && spec.from[theTemplateParam]) { + for (const ground of this._metafiedTypes) { + this._metaTyped.addConversion( + {from: ground, to: base, convert: spec.from[theTemplateParam]}) + } + } this._instantiationsOf[base] = new Set() // update the typeOf function @@ -750,45 +785,39 @@ export default class PocomathInstance { /** * Reset an operation to require creation of typed-function, * and if it has no implementations so far, set them up. + * name is the name of the operation, badType is a type that has been + * invalidated, and reasons is a set of specific operations/signatures + * that have been invalidated */ - _invalidate(name, reason) { + _invalidate(name, badType = '', reasons = new Set()) { if (!(name in this._imps)) { this._imps[name] = {} this._TFimps[name] = {} this._metaTFimps[name] = {} } - if (reason) { - // Make sure no TF imps that depend on reason remain: - for (const [signature, behavior] of Object.entries(this._imps[name])) { - let invalidated = false - if (reason.charAt(0) === ':') { - const badType = reason.slice(1) - if (signature.includes(badType)) invalidated = true + // Go through each TF imp and invalidate it if need be + for (const [signature, imp] of Object.entries(this._TFimps[name])) { + if (imp.deferred + || (badType && signature.includes(badType)) + || exists(imp.uses, dep => { + const [func, sig] = dep.split(/[()]/) + return reasons.has(dep) + || (reasons.has(func) && !(sig in this._TFimps[func])) + })) { + // Invalidate this implementation: + delete this._TFimps[name][signature] + const behavior = imp.fromBehavior + if (behavior.explicit) { + behavior.resolved = false } else { - for (const dep of behavior.uses) { - if (dep.includes(reason)) { - invalidated = true - break - } - } - } - if (invalidated) { - if (behavior.explicit) { - if (behavior.resolved) delete this._TFimps[signature] - behavior.resolved = false - } else { - for (const fullSig - of Object.values(behavior.hasInstantiations)) { - delete this._TFimps[fullSig] - } - behavior.hasInstantiations = {} - } + delete behavior.hasInstantiations[imp.instance] } + reasons.add(`${name}(${signature})`) } } if (this._invalid.has(name)) return this._invalid.add(name) - this._invalidateDependents(name) + this._invalidateDependents(name, badType, reasons) const self = this Object.defineProperty(this, name, { configurable: true, @@ -802,11 +831,14 @@ export default class PocomathInstance { /** * Invalidate all the dependents of a given property of the instance + * reasons is a set of invalidated signatures */ - _invalidateDependents(name) { + _invalidateDependents(name, badType, reasons = new Set()) { + if (name.charAt(0) === ':') badType = name.slice(1) + else reasons.add(name) if (name in this._affects) { for (const ancestor of this._affects[name]) { - this._invalidate(ancestor, name) + this._invalidate(ancestor, badType, reasons) } } } @@ -847,7 +879,7 @@ export default class PocomathInstance { for (const [rawSignature, behavior] of usableEntries) { if (behavior.explicit) { if (!(behavior.resolved)) { - this._addTFimplementation(tf_imps, rawSignature, behavior) + this._addTFimplementation(name, tf_imps, rawSignature, behavior) tf_imps[rawSignature]._pocoSignature = rawSignature behavior.resolved = true } @@ -867,11 +899,18 @@ export default class PocomathInstance { } /* First, add the known instantiations, gathering all types needed */ if (ubType) behavior.needsInstantiations.add(ubType) + const nargs = typeListOfSignature(rawSignature).length let instantiationSet = new Set() const ubTypes = new Set() if (!ubType) { // Collect all upper-bound types for this signature for (const othersig in imps) { + const otherNargs = typeListOfSignature(othersig).length + if (nargs !== otherNargs) { + // crude criterion that it won't match, that ignores + // rest args, but hopefully OK for prototype + continue + } const thisUB = upperBounds.exec(othersig) if (thisUB) ubTypes.add(thisUB[2]) let basesig = othersig.replaceAll(templateCall, '') @@ -881,7 +920,7 @@ export default class PocomathInstance { basesig, theTemplateParam, '') if (testsig === basesig) { // that is not also top-level - for (const templateType of typeListOfSignature(basesig)) { + for (let templateType of typeListOfSignature(basesig)) { if (templateType.slice(0,3) === '...') { templateType = templateType.slice(3) } @@ -894,21 +933,20 @@ export default class PocomathInstance { for (const instType of behavior.needsInstantiations) { instantiationSet.add(instType) const otherTypes = - ubType ? this.subtypesOf(instType) : this._priorTypes[instType] + ubType ? this.subtypesOf(instType) : this._priorTypes[instType] for (const other of otherTypes) { if (!(this._atOrBelowSomeType(other, ubTypes))) { instantiationSet.add(other) } } } - /* Prevent other existing signatures from blocking use of top-level * templates via conversions: */ let baseSignature = rawSignature.replaceAll(templateCall, '') /* Any remaining template params are top-level */ const signature = substituteInSignature( - baseSignature, theTemplateParam, 'any') + baseSignature, theTemplateParam, UniversalType) const hasTopLevel = (signature !== baseSignature) if (!ubType && hasTopLevel) { for (const othersig in imps) { @@ -939,9 +977,9 @@ export default class PocomathInstance { } } } - for (const instType of instantiationSet) { - this._instantiateTemplateImplementation(name, rawSignature, instType) + this._instantiateTemplateImplementation( + name, rawSignature, instType) } /* Now add the catchall signature */ /* (Not needed if if it's a bounded template) */ @@ -996,6 +1034,7 @@ export default class PocomathInstance { `Type inference failed for argument ${j} of ${name}`) } if (argType === 'any') { + console.log('INCOMPATIBLE ARGUMENTS are', args) throw TypeError( `In call to ${name}, ` + 'incompatible template arguments:' @@ -1018,9 +1057,10 @@ export default class PocomathInstance { usedConversions = true instantiateFor = self.joinTypes(argTypes, usedConversions) if (instantiateFor === 'any') { + let argDisplay = args.map(toString).join(', ') throw TypeError( `In call to ${name}, no type unifies arguments ` - + args.toString() + '; of types ' + argTypes.toString() + + argDisplay + '; of types ' + argTypes.toString() + '; note each consecutive pair must unify to a ' + 'supertype of at least one of them') } @@ -1042,7 +1082,9 @@ export default class PocomathInstance { for (j = 0; j < parTypes.length; ++j) { if (wantTypes[j] !== parTypes[j] && parTypes[j].includes('<')) { // actually used the param and is a template - self._ensureTemplateTypes(parTypes[j], instantiateFor) + const strippedType = parTypes[j].substr( + parTypes[j].lastIndexOf('.') + 1) + self._ensureTemplateTypes(strippedType, instantiateFor) } } @@ -1050,12 +1092,19 @@ export default class PocomathInstance { // But possibly since this resolution was grabbed, the proper // instantiation has been added (like if there are multiple // uses in the implementation of another method. - if (!(behavior.needsInstantiations.has(instantiateFor))) { - behavior.needsInstantiations.add(instantiateFor) + let whatToDo + if (!(instantiateFor in behavior.hasInstantiations)) { + const newImp = self._instantiateTemplateImplementation( + name, rawSignature, instantiateFor) + if (newImp) { + whatToDo = {fn: newImp, implementation: newImp} + } self._invalidate(name) } const brandNewMe = self[name] - const whatToDo = self._typed.resolve(brandNewMe, args) + const betterToDo = self._typed.resolve(brandNewMe, args) + whatToDo = betterToDo || whatToDo + // We can access return type information here // And in particular, if it might be a template, we should try to // instantiate it: @@ -1069,8 +1118,13 @@ export default class PocomathInstance { } if (whatToDo === lastWhatToDo) { throw new Error( - `Infinite recursion in resolving $name called on` - + args.map(x => x.toString()).join(',')) + `Infinite recursion in resolving ${name} called on ` + + args.map(x => + (typeof x === 'object' + ? JSON.stringify(x) + : x.toString()) + ).join(', ') + + ` inferred to be ${wantSig}`) } lastWhatToDo = whatToDo const retval = whatToDo.implementation(...args) @@ -1088,15 +1142,19 @@ export default class PocomathInstance { // correct return type a priori. Deferring because unclear what // aspects will be merged into typed-function. this._addTFimplementation( - meta_imps, signature, {uses: new Set(), does: patch}) + name, meta_imps, signature, + {uses: new Set(), does: patch}, + behavior) behavior.resolved = true } - this._correctPartialSelfRefs(name, tf_imps) // Make sure we have all of the needed (template) types; and if they // can't be added (because they have been instantiated too deep), // ditch the signature: const badSigs = new Set() for (const sig in tf_imps) { + if (!tf_imps[sig].uses) { + throw new ReferenceError(`MONKEY WRENCH: ${name} ${sig}`) + } for (const type of typeListOfSignature(sig)) { if (this._maybeInstantiate(type) === undefined) { badSigs.add(sig) @@ -1117,11 +1175,13 @@ export default class PocomathInstance { if (Object.keys(tf_imps).length > 0) { tf = this._typed(name, tf_imps) tf.fromInstance = this + tf.isMeta = false } let metaTF if (Object.keys(meta_imps).length > 0) { metaTF = this._metaTyped(name, meta_imps) metaTF.fromInstance = this + metaTF.isMeta = true } this._meta[name] = metaTF @@ -1215,10 +1275,12 @@ export default class PocomathInstance { return behavior.does(innerRefs) } const tf_imps = this._TFimps[name] - this._addTFimplementation(tf_imps, signature, {uses, does: patch}) + this._addTFimplementation( + name, tf_imps, signature, {uses, does: patch}, behavior, instanceType) tf_imps[signature]._pocoSignature = templateSignature tf_imps[signature]._pocoInstance = instanceType behavior.hasInstantiations[instanceType] = signature + behavior.needsInstantiations.add(instanceType) // once we have it, keep it return tf_imps[signature] } @@ -1226,17 +1288,23 @@ export default class PocomathInstance { * to typed-function implementations and inserts the result into plain * object imps */ - _addTFimplementation(imps, signature, behavior) { - const {uses, does} = behavior + _addTFimplementation( + name, imps, signature, specificBehavior, fromImp, asInstance) + { + if (!fromImp) fromImp = specificBehavior + const {uses, does} = specificBehavior if (uses.length === 0) { const implementation = does() + implementation.uses = uses + implementation.fromInstance = this + implementation.fromBehavior = fromImp + implementation.instance = asInstance // could do something with return type information here imps[signature] = implementation return } const refs = {} let full_self_referential = false - let part_self_references = [] for (const dep of uses) { let [func, needsig] = dep.split(/[()]/) /* Safety check that can perhaps be removed: @@ -1252,82 +1320,71 @@ export default class PocomathInstance { } if (func === 'self') { if (needsig) { - /* Maybe we can resolve the self reference without troubling - * typed-function: + /* We now resolve all specific-signature self references + * here, without resorting to the facility in typed-function: */ if (needsig in imps && typeof imps[needsig] == 'function') { refs[dep] = imps[needsig] + continue + } + const needTypes = typesOfSignature(needsig) + const mergedTypes = Object.assign( + {}, this.Types, this.Templates) + if (subsetOfKeys(needTypes, mergedTypes)) { + func = name // just resolve it in limbo } else { - if (full_self_referential) { - throw new SyntaxError( - 'typed-function does not support mixed full and ' - + 'partial self-reference') - } - const needTypes = typesOfSignature(needsig) - const mergedTypes = Object.assign( - {}, this.Types, this.Templates) - if (subsetOfKeys(needTypes, mergedTypes)) { - part_self_references.push(needsig) - } + // uses an unknown type, so will get an undefined impl + console.log( + 'WARNING: partial self-reference for', name, 'to', + needsig, 'uses an unknown type') + refs[dep] = undefined + continue } } 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 */ - let fallback = true - /* So the first thing we can do is try the tf_imps we are - * accumulating: - */ - if (needsig) { - let typedUniverse - let tempTF - if (Object.keys(this._TFimps[func]).length > 0) { - typedUniverse = this._typed - tempTF = typedUniverse('dummy_' + func, this._TFimps[func]) - } else { - typedUniverse = this._metaTyped - tempTF = typedUniverse( - 'dummy_' + func, this._metaTFimps[func]) - } - let result = undefined - try { - result = typedUniverse.find(tempTF, needsig, {exact: true}) - } catch {} - if (result) { - refs[dep] = result - fallback = false - } - } - if (fallback) { - /* Either we need the whole function or the signature - * we need is not available yet, so we 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 - const redirect = function () { // is this the most efficient? - return self[func].apply(this, arguments) - } - Object.defineProperty(redirect, 'name', {value: func}) - Object.defineProperty(redirect, 'fromInstance', {value: this}) - refs[dep] = redirect - } - } else { - // can bundle up func, and grab its signature if need be - let destination = this[func] - if (destination && needsig) { - destination = this.resolve(func, needsig) - } - refs[dep] = destination + continue } } + if (this[func] === 'limbo') { + /* We are in the midst of bundling func (which may be ourself) */ + /* So the first thing we can do is try the tf_imps we are + * accumulating: + */ + if (needsig) { + const candidate = this.resolve(func, needsig) + if (typeof candidate === 'function') { + refs[dep] = candidate + continue + } + } + /* Either we need the whole function or the signature + * we need is not available yet, so we 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 + const redirect = function () { // is this the most efficient? + return self[func].apply(this, arguments) + } + Object.defineProperty(redirect, 'name', {value: func}) + Object.defineProperty(redirect, 'fromInstance', {value: this}) + refs[dep] = redirect + continue + } + // can bundle up func, and grab its signature if need be + let destination = this[func] + if (needsig) { + destination = this.resolve(func, needsig) + } + if (!destination) { + // Unresolved reference. This is allowed so that + // you can bundle up just some portions of the library, + // but let's warn. + console.log( + 'WARNING: No definition found for dependency', + dep, 'needed by', name, '(', signature, ')') + } + refs[dep] = destination } if (full_self_referential) { imps[signature] = this._typed.referToSelf(self => { @@ -1335,108 +1392,27 @@ export default class PocomathInstance { const implementation = does(refs) Object.defineProperty(implementation, 'name', {value: does.name}) implementation.fromInstance = this + implementation.uses = uses + implementation.instance = asInstance + implementation.fromBehavior = fromImp // What are we going to do with the return type info in here? return implementation }) - return - } - if (part_self_references.length) { - /* There is an obstruction here. The list part_self_references - * might contain a signature that requires conversion for self to - * handle. But I advocated this not be allowed in typed.referTo, which - * made sense for human-written functions, but is unfortunate now. - * So we have to defer creating these and correct them later, at - * least until we can add an option to typed-function. - */ - imps[signature] = { - deferred: true, - builtRefs: refs, - sigDoes: does, - fromInstance: this, - psr: part_self_references - } + imps[signature].uses = uses + imps[signature].fromInstance = this + imps[signature].instance = asInstance + imps[signature].fromBehavior = fromImp return } const implementation = does(refs) implementation.fromInstance = this + implementation.fromBehavior = fromImp + implementation.instance = asInstance + implementation.uses = uses // could do something with return type information here? imps[signature] = implementation } - _correctPartialSelfRefs(name, imps) { - for (const aSignature in imps) { - if (!(imps[aSignature].deferred)) continue - const deferral = imps[aSignature] - const part_self_references = deferral.psr - const corrected_self_references = [] - const remaining_self_references = [] - const refs = deferral.builtRefs - for (const neededSig of part_self_references) { - // Have to find a match for neededSig among the other signatures - // of this function. That's a job for typed-function, but we will - // try here: - if (neededSig in imps) { // the easy case - corrected_self_references.push(neededSig) - remaining_self_references.push(neededSig) - continue - } - // No exact match, try to get one that matches with - // subtypes since the whole conversion thing in typed-function - // is too complicated to reproduce - let foundSig = this._findSubtypeImpl(name, imps, neededSig) - if (foundSig) { - corrected_self_references.push(foundSig) - remaining_self_references.push(neededSig) - } else { - // Maybe it's a template instance we don't yet have - foundSig = this._findSubtypeImpl( - name, this._imps[name], neededSig) - if (foundSig) { - const match = this._pocoFindSignature(name, neededSig) - const neededTemplate = match.fn._pocoSignature - const neededInstance = whichSigInstance( - neededSig, neededTemplate) - const neededImplementation = - this._instantiateTemplateImplementation( - name, neededTemplate, neededInstance) - if (!neededImplementation) { - refs[`self(${neededSig})`] = match.implementation - } else { - if (typeof neededImplementation === 'function') { - refs[`self(${neededSig})`] = neededImplementation - } else { - corrected_self_references.push(neededSig) - remaining_self_references.push(neededSig) - } - } - } else { - throw new Error( - 'Implement inexact self-reference in typed-function for ' - + `${name}(${neededSig})`) - } - } - } - const does = deferral.sigDoes - if (remaining_self_references.length > 0) { - imps[aSignature] = this._typed.referTo( - ...corrected_self_references, (...impls) => { - for (let i = 0; i < remaining_self_references.length; ++i) { - refs[`self(${remaining_self_references[i]})`] = impls[i] - } - const implementation = does(refs) - // What will we do with the return type info in here? - return implementation - } - ) - } else { - imps[aSignature] = does(refs) - } - imps[aSignature]._pocoSignature = deferral._pocoSignature - imps[aSignature]._pocoInstance = deferral._pocoInstance - imps[aSignature].fromInstance = deferral.fromInstance - } - } - /* This function analyzes the template and makes sure the * instantiations of it for type and all prior types of type are present * in the instance. @@ -1542,7 +1518,8 @@ export default class PocomathInstance { return wantsType }) - _findSubtypeImpl(name, imps, neededSig) { + _findSubtypeImpl(name, imps, neededSig, raw = false) { + const detemplate = !raw if (neededSig in imps) return neededSig let foundSig = false const typeList = typeListOfSignature(neededSig) @@ -1550,21 +1527,21 @@ export default class PocomathInstance { const otherTypeList = typeListOfSignature(otherSig) if (typeList.length !== otherTypeList.length) continue let allMatch = true - let paramBound = 'any' + let paramBound = UniversalType for (let k = 0; k < typeList.length; ++k) { let myType = typeList[k] let otherType = otherTypeList[k] if (otherType === theTemplateParam) { - otherTypeList[k] = paramBound + if (detemplate) otherTypeList[k] = paramBound otherType = paramBound } if (otherType === restTemplateParam) { - otherTypeList[k] = `...${paramBound}` + if (detemplate) otherTypeList[k] = `...${paramBound}` otherType = paramBound } const adjustedOtherType = otherType.replaceAll(templateCall, '') if (adjustedOtherType !== otherType) { - otherTypeList[k] = adjustedOtherType + if (detemplate) otherTypeList[k] = adjustedOtherType otherType = adjustedOtherType } if (myType.slice(0,3) === '...') myType = myType.slice(3) @@ -1573,10 +1550,13 @@ export default class PocomathInstance { if (otherBound) { paramBound = otherBound[2] otherType = paramBound - otherTypeList[k] = otherBound[1].replaceAll( - theTemplateParam, paramBound) + if (detemplate) { + otherTypeList[k] = otherBound[1].replaceAll( + theTemplateParam, paramBound) + } } if (otherType === 'any') continue + if (otherType === UniversalType) continue if (myType === otherType) continue if (otherType in this.Templates) { const [myBase] = splitTemplate(myType) @@ -1584,7 +1564,7 @@ export default class PocomathInstance { if (this.instantiateTemplate(otherType, myType)) { let dummy dummy = this[name] // for side effects - return this._findSubtypeImpl(name, this._imps[name], neededSig) + return this._findSubtypeImpl(name, this._imps[name], neededSig, raw) } } if (!(otherType in this.Types)) { @@ -1608,6 +1588,7 @@ export default class PocomathInstance { typedFunction = this[name] } const haveTF = this._typed.isTypedFunction(typedFunction) + && !(typedFunction.isMeta) if (haveTF) { // First try a direct match let result @@ -1622,6 +1603,10 @@ export default class PocomathInstance { of typedFunction._typedFunctionData.signatureMap) { let allMatched = true const implTypes = typeListOfSignature(implSig) + if (implTypes.length > wantTypes.length) { + // Not enough arguments for that implementation + continue + } for (let i = 0; i < wantTypes.length; ++i) { const implIndex = Math.min(i, implTypes.length - 1) let implType = implTypes[implIndex] @@ -1646,7 +1631,7 @@ export default class PocomathInstance { } } if (!(this._imps[name])) return undefined - const foundsig = this._findSubtypeImpl(name, this._imps[name], sig) + const foundsig = this._findSubtypeImpl(name, this._imps[name], sig, 'raw') if (foundsig) { if (haveTF) { try { @@ -1654,19 +1639,74 @@ export default class PocomathInstance { } catch { } } - try { - return this._metaTyped.findSignature(this._meta[name], foundsig) - } catch { + const instantiationMatcher = + '^' + + substituteInSignature(foundsig, theTemplateParam, '(.*)') + .replaceAll(UniversalType, '(.*)') + + '$' + const instanceMatch = sig.match(instantiationMatcher) + let possibleInstantiator = false + if (instanceMatch) { + possibleInstantiator = instanceMatch[1] + for (let i = 2; i < instanceMatch.length; ++i) { + if (possibleInstantiator !== instanceMatch[i]) { + possibleInstantiator = false + break + } + } + } + if (possibleInstantiator) { + const behavior = this._imps[name][foundsig] + let newInstance + if (behavior) { + if (!(possibleInstantiator in behavior.hasInstantiations)) { + newInstance = this._instantiateTemplateImplementation( + name, foundsig, possibleInstantiator) + } else { + // OK, so we actually have the instantiation. Let's get it + newInstance = this._TFimps[name][sig] + } + // But we may not have taken advantage of conversions + this._invalidate(name) + const tryAgain = this[name] + let betterInstance + if (this._typed.isTypedFunction(tryAgain)) { + betterInstance = this._typed.findSignature(tryAgain, sig) + } + if (betterInstance) { + newInstance = betterInstance + } else { + newInstance = { + fn: newInstance, + implementation: newInstance + } + } + if (newInstance) return newInstance + } + } + const catchallSig = this._findSubtypeImpl(name, this._imps[name], sig) + if (catchallSig !== foundsig) { + try { + return this._metaTyped.findSignature( + this._meta[name], catchallSig) + } catch { + } } // We have an implementation but not a typed function. Do the best // we can: - const foundImpl = this._imps[name][foundsig] + const restoredSig = foundsig.replaceAll('ground', theTemplateParam) + const foundImpl = this._imps[name][restoredSig] const needs = {} for (const dep of foundImpl.uses) { - const [base, sig] = dep.split('()') - needs[dep] = this.resolve(base, sig) + const [base, sig] = dep.split(/[()]/) + if (sig) { + needs[dep] = this.resolve(base, sig) + } else { + needs[dep] = this[dep] + } } const pseudoImpl = foundImpl.does(needs) + pseudoImpl.fromInstance = this return {fn: pseudoImpl, implementation: pseudoImpl} } // Hmm, no luck. Make sure bundle is up-to-date and retry: diff --git a/src/core/extractors.mjs b/src/core/extractors.mjs index 0db3c0f..f463cd1 100644 --- a/src/core/extractors.mjs +++ b/src/core/extractors.mjs @@ -6,7 +6,7 @@ export function dependencyExtractor(destinationSet) { return new Proxy({}, { get: (target, property) => { destinationSet.add(property) - return {} + return {checkingDependency: true} } }) } diff --git a/src/number/add.mjs b/src/number/add.mjs index 41791cf..5c363d1 100644 --- a/src/number/add.mjs +++ b/src/number/add.mjs @@ -4,5 +4,5 @@ export * from './Types/number.mjs' export const add = { // Note the below assumes that all subtypes of number that will be defined // are closed under addition! - 'T:number, T': ({T}) => Returns(T, (m,n) => m+n) + 'T:number,T': ({T}) => Returns(T, (m,n) => m+n) } diff --git a/src/number/cbrt.mjs b/src/number/cbrt.mjs new file mode 100644 index 0000000..af7587b --- /dev/null +++ b/src/number/cbrt.mjs @@ -0,0 +1,19 @@ +import Returns from '../core/Returns.mjs' +export * from './Types/number.mjs' + +/* Returns just the real cube root, following mathjs implementation */ +export const cbrt = { + number: ({'negate(number)': neg}) => Returns('number', x => { + if (x === 0) return x + const negate = x < 0 + if (negate) x = neg(x) + let result = x + if (isFinite(x)) { + result = Math.exp(Math.log(x) / 3) + result = (x / (result * result) + (2 * result)) / 3 + } + if (negate) return neg(result) + return result + }) +} + diff --git a/src/number/native.mjs b/src/number/native.mjs index fcebecc..ad2de12 100644 --- a/src/number/native.mjs +++ b/src/number/native.mjs @@ -6,6 +6,7 @@ export * from './Types/number.mjs' export {abs} from './abs.mjs' export {absquare} from './absquare.mjs' export {add} from './add.mjs' +export {cbrt} from './cbrt.mjs' export {compare} from './compare.mjs' export const conjugate = {'T:number': identitySubTypes('number')} export const gcd = gcdType('NumInt') diff --git a/src/tuple/tuple.mjs b/src/tuple/tuple.mjs index 9cd0c65..8467176 100644 --- a/src/tuple/tuple.mjs +++ b/src/tuple/tuple.mjs @@ -6,5 +6,5 @@ export {Tuple} from './Types/Tuple.mjs' * are convertible to the same type. */ export const tuple = { - '...T': ({T}) => Returns(`Tuple<${T}>`, args => ({elts: args})) + '...T': ({T}) => Returns(`Tuple<${T}>`, args => ({elts: args})) } diff --git a/test/complex/_all.mjs b/test/complex/_all.mjs index 413503c..a14872e 100644 --- a/test/complex/_all.mjs +++ b/test/complex/_all.mjs @@ -45,6 +45,11 @@ describe('complex', () => { assert.ok(!(math.equal(math.complex(45n, 3n), 45n))) }) + it('tests for reality', () => { + assert.ok(math.isReal(math.complex(3, 0))) + assert.ok(!(math.isReal(math.complex(3, 2)))) + }) + it('computes gcd', () => { assert.deepStrictEqual( math.gcd(math.complex(53n, 56n), math.complex(47n, -13n)), diff --git a/test/complex/_polynomialRoot.mjs b/test/complex/_polynomialRoot.mjs new file mode 100644 index 0000000..22ad90c --- /dev/null +++ b/test/complex/_polynomialRoot.mjs @@ -0,0 +1,63 @@ +import assert from 'assert' +import * as approx from '../../tools/approx.mjs' +import math from '../../src/pocomath.mjs' + +describe('polynomialRoot', () => { + it('should solve a linear equation with real coefficients', function () { + assert.deepEqual(math.polynomialRoot(6, 3), math.tuple(-2)) + assert.deepEqual( + math.polynomialRoot(math.complex(-3, 2), 2), + math.tuple(math.complex(1.5, -1))) + assert.deepEqual( + math.polynomialRoot(math.complex(3, 1), math.complex(-1, -1)), + math.tuple(math.complex(2, -1))) + }) + // Should be safe now to capture the functions: + const complex = math.complex + const pRoot = math.polynomialRoot + const tup = math.tuple + it('should solve a quadratic equation with a double root', function () { + assert.deepEqual(pRoot(4, 4, 1), tup(-2)) + assert.deepEqual( + pRoot(complex(0, 2), complex(2, 2), 1), tup(complex(-1, -1))) + }) + it('should solve a quadratic with two distinct roots', function () { + assert.deepEqual(pRoot(-3, 2, 1), tup(1, -3)) + assert.deepEqual(pRoot(-2, 0, 1), tup(math.sqrt(2), -math.sqrt(2))) + assert.deepEqual( + pRoot(4, 2, 1), + tup(complex(-1, math.sqrt(3)), complex(-1, -math.sqrt(3)))) + assert.deepEqual( + pRoot(complex(3, 1), -3, 1), tup(complex(1, 1), complex(2, -1))) + }) + it('should solve a cubic with a triple root', function () { + assert.deepEqual(pRoot(8, 12, 6, 1), tup(-2)) + assert.deepEqual( + pRoot(complex(-2, 11), complex(9, -12), complex(-6, 3), 1), + tup(complex(2, -1))) + }) + it('should solve a cubic with one simple and one double root', function () { + assert.deepEqual(pRoot(4, 0, -3, 1), tup(-1, 2)) + assert.deepEqual( + pRoot(complex(9, 9), complex(15, 6), complex(7, 1), 1), + tup(complex(-1, -1), -3)) + assert.deepEqual( + pRoot(complex(0, 6), complex(6, 8), complex(5, 2), 1), + tup(-3, complex(-1, -1))) + assert.deepEqual( + pRoot(complex(2, 6), complex(8, 6), complex(5, 1), 1), + tup(complex(-3, 1), complex(-1, -1))) + }) + it('should solve a cubic with three distinct roots', function () { + approx.deepEqual(pRoot(6, 11, 6, 1), tup(-3, -1, -2)) + approx.deepEqual( + pRoot(-1, -2, 0, 1), + tup(-1, (1 + math.sqrt(5)) / 2, (1 - math.sqrt(5)) / 2)) + approx.deepEqual( + pRoot(1, 1, 1, 1), + tup(-1, complex(0, -1), complex(0, 1))) + approx.deepEqual( + pRoot(complex(0, -10), complex(8, 12), complex(-6, -3), 1), + tup(complex(1, 1), complex(3, 1), complex(2, 1))) + }) +}) diff --git a/tools/approx.mjs b/tools/approx.mjs new file mode 100644 index 0000000..cab5483 --- /dev/null +++ b/tools/approx.mjs @@ -0,0 +1,46 @@ +import assert from 'assert' + +export const epsilon = 1e-12 + +const isNumber = entity => (typeof entity === 'number') + +export function equal(a, b) { + if (isNumber(a) && isNumber(b)) { + if (a === b) return true + if (isNaN(a)) return assert.strictEqual(a.toString(), b.toString()) + const message = `${a} ~= ${b} (to ${epsilon})` + if (a === 0) return assert.ok(Math.abs(b) < epsilon, message) + if (b === 0) return assert.ok(Math.abs(a) < epsilon, message) + const diff = Math.abs(a - b) + const maxDiff = Math.abs(epsilon * Math.max(Math.abs(a), Math.abs(b))) + return assert.ok(diff <= maxDiff, message) + } + return assert.strictEqual(a, b) +} + +export function deepEqual(a, b) { + if (Array.isArray(a) && Array.isArray(b)) { + const alen = a.length + assert.strictEqual(alen, b.length, `${a} ~= ${b}`) + for (let i = 0; i < alen; ++i) deepEqual(a[i], b[i]) + return true + } + if (typeof a === 'object' && typeof b === 'object') { + for (const prop in a) { + if (a.hasOwnProperty(prop)) { + assert.ok( + b.hasOwnProperty(prop), `a[${prop}] = ${a[prop]} ~= ${b[prop]}`) + deepEqual(a[prop], b[prop]) + } + } + + for (const prop in b) { + if (b.hasOwnProperty(prop)) { + assert.ok( + a.hasOwnProperty(prop), `${a[prop]} ~= ${b[prop]} = b[${prop}]`) + } + } + return true + } + return equal(a, b) +}