From 7ec96860197f16ce58a4b18619f63202f6b0df02 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Fri, 5 Aug 2022 05:44:28 -0700 Subject: [PATCH] feat: Homogeneous typed Tuple class using templates --- src/bigint/native.mjs | 2 + src/complex/add.mjs | 8 +- src/complex/associate.mjs | 2 +- src/complex/complex.mjs | 3 +- src/complex/{equal.mjs => equalTT.mjs} | 2 +- src/complex/native.mjs | 2 +- src/core/PocomathInstance.mjs | 204 ++++++++++++++++--------- src/core/utils.mjs | 5 + src/generic/arithmetic.mjs | 1 + src/generic/identity.mjs | 3 + src/generic/relational.mjs | 19 ++- src/number/native.mjs | 2 + src/ops/floor.mjs | 2 +- src/pocomath.mjs | 10 +- src/tuple/Types/Tuple.mjs | 26 ++-- src/tuple/equal.mjs | 14 -- src/tuple/equalTT.mjs | 11 ++ src/tuple/native.mjs | 5 +- test/custom.mjs | 18 +-- test/tuple/_native.mjs | 88 ++++++++++- 20 files changed, 291 insertions(+), 136 deletions(-) rename src/complex/{equal.mjs => equalTT.mjs} (95%) create mode 100644 src/generic/identity.mjs delete mode 100644 src/tuple/equal.mjs create mode 100644 src/tuple/equalTT.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 index c6fab9e..f3106b4 100644 --- a/src/complex/associate.mjs +++ b/src/complex/associate.mjs @@ -4,7 +4,7 @@ export * from './Types/Complex.mjs' export const associate = { 'Complex,Complex': ({ 'multiply(Complex,Complex)': times, - 'equal(Complex,Complex)': eq, + 'equalTT(Complex,Complex)': eq, zero, one, complex, 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/native.mjs b/src/complex/native.mjs index 1dfb45e..420ce88 100644 --- a/src/complex/native.mjs +++ b/src/complex/native.mjs @@ -6,7 +6,7 @@ export {add} from './add.mjs' export {associate} from './associate.mjs' export {complex} from './complex.mjs' export {conjugate} from './conjugate.mjs' -export {equal} from './equal.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 cc999c1..05636d4 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -2,7 +2,7 @@ import typed from 'typed-function' import {dependencyExtractor, generateTypeExtractor} from './extractors.mjs' import {makeChain} from './Chain.mjs' -import {typesOfSignature, subsetOfKeys} from './utils.mjs' +import {typeListOfSignature, typesOfSignature, subsetOfKeys} from './utils.mjs' const anySpec = {} // fixed dummy specification of 'any' type @@ -30,6 +30,8 @@ export default class PocomathInstance { 'installType', 'joinTypes', 'name', + 'self', + 'Templates', 'typeOf', 'Types', 'undefinedTypes' @@ -41,13 +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._templateTypes = {} + 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. @@ -56,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, { @@ -72,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) } /** @@ -181,10 +193,10 @@ 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._templateTypes)) { + for (const [base, info] of Object.entries(other.Templates)) { this._installTemplateType(info.type, info.spec) } const migrateImps = {} @@ -290,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 @@ -303,17 +315,14 @@ 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, and - // while we are at it add trivial conversions from subtypes to supertypes - // to help typed-function match signatures properly: + // 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) @@ -325,6 +334,7 @@ export default class PocomathInstance { // 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) @@ -384,6 +394,7 @@ export default class PocomathInstance { 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 @@ -409,7 +420,7 @@ export default class PocomathInstance { * 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 */ @@ -418,15 +429,15 @@ export default class PocomathInstance { /* For now, just allow a single template per base type; that * might need to change later: */ - if (base in this._templateTypes) { - if (spec !== this._templateTypes[base].spec) { + 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._templateTypes[base] = {type, spec} + this.Templates[base] = {type, spec} } /* Used internally by install, see the documentation there */ @@ -469,7 +480,7 @@ export default class PocomathInstance { for (const word of type.split(/[<>]/)) { if (word.length == 0) continue if (this._templateParam(word)) continue - this._usedTypes.add(word) + this._seenTypes.add(word) this._addAffect(':' + word, name) } } @@ -497,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 + } }) } @@ -541,7 +552,7 @@ export default class PocomathInstance { for (const type of typesOfSignature(entry[0])) { if (type in this.Types) continue const baseType = type.split('<')[0] - if (baseType in this._templateTypes) continue + if (baseType in this.Templates) continue keep = false break } @@ -578,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) } } @@ -595,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() @@ -622,15 +627,15 @@ export default class PocomathInstance { /* Now add the catchall signature */ let templateCall = `<${theTemplateParam}>` /* Relying here that the base of 'Foo' is 'Foo': */ - let baseSignature = trimSignature.replaceAll(templateCall, '') + let baseSignature = rawSignature.replaceAll(templateCall, '') /* Any remaining template params are top-level */ const signature = substituteInSig( - baseSignature, theTemplateParam, 'any') + baseSignature, theTemplateParam, 'ground') /* The catchall signature has to detect the actual type of the call * and add the new instantiations. * First, prepare the type inference data: */ - const parTypes = trimSignature.split(',') + const parTypes = rawSignature.split(',') const restParam = (parTypes[parTypes.length-1].slice(0,3) === '...') const topTyper = entity => this.typeOf(entity) const inferences = parTypes.map( @@ -639,7 +644,7 @@ export default class PocomathInstance { theTemplateParam, topTyper, this.joinTypes.bind(this), - this._templateTypes)) + this.Templates)) if (inferences.every(x => !x)) { // all false throw new SyntaxError( `Cannot find template parameter in ${rawSignature}`) @@ -700,7 +705,6 @@ export default class PocomathInstance { usedConversions = true instantiateFor = self.joinTypes(argTypes, usedConversions) if (instantiateFor === 'any') { - // Need a more informative error message here throw TypeError( `In call to ${name}, no type unifies arguments ` + args.toString() + '; of types ' + argTypes.toString() @@ -717,7 +721,7 @@ export default class PocomathInstance { * by instantiateFor, and for all of instantiateFor's "prior types" */ for (j = 0; j < parTypes.length; ++j) { - if (wantTypes[j] !== parTypes[j] && wantTypes[j].includes('<')) { + if (wantTypes[j] !== parTypes[j] && parTypes[j].includes('<')) { // actually used the param and is a template self._ensureTemplateTypes(parTypes[j], instantiateFor) } @@ -762,8 +766,9 @@ export default class PocomathInstance { if (self._typed.isTypedFunction(refs[simplifiedDep])) { const subsig = substituteInSig( needsig, theTemplateParam, instantiateFor) - innerRefs[dep] = self._typed.find( - refs[simplifiedDep], subsig) + let resname = simplifiedDep + if (resname === 'self') resname = name + innerRefs[dep] = self._pocoresolve(resname, subsig) } else { innerRefs[dep] = refs[simplifiedDep] } @@ -841,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 } @@ -886,27 +891,10 @@ export default class PocomathInstance { corrected_self_references.push(neededSig) continue } - // No exact match, have to try to get one that matches with + // 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 = false - const typeList = typesOfSignature(neededSig) - for (const otherSig in imps) { - const otherTypeList = typesOfSignature(otherSig) - if (typeList.length !== otherTypeList.length) continue - const allMatch = true - for (let k = 0; k < typeList.length; ++k) { - if (this._subtypes[otherTypeList[k]].has(typeList[k])) { - continue - } - allMatch = false - break - } - if (allMatch) { - foundSig = otherSig - break - } - } + const foundSig = this._findSubtypeImpl(imps, neededSig) if (foundSig) { corrected_self_references.push(foundSig) } else { @@ -963,18 +951,14 @@ 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 = {} - const template = this._templateTypes[base].spec + 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] - if (instantiatorSpec.refines) { - // Assuming all templates are covariant, for now - this._maybeAddTemplateType(base, instantiatorSpec.refines) - newTypeSpec.refines = `${base}<${instantiatorSpec.refines}>` - } let beforeTypes = [] if (instantiatorSpec.before) { beforeTypes = instantiatorSpec.before.map(type => `${base}<${type}>`) @@ -988,9 +972,8 @@ export default class PocomathInstance { if (beforeTypes.length > 0) { newTypeSpec.before = beforeTypes } - newTypeSpec.test = template.test(instantiatorSpec.test) + newTypeSpec.test = template.test(this._typeTests[instantiator]) if (template.from) { - newTypeSpec.from = {} for (let source in template.from) { const instSource = substituteInSig( source, theTemplateParam, instantiator) @@ -1005,16 +988,87 @@ export default class PocomathInstance { for (const iFrom in instantiatorSpec.from) { const finalSource = substituteInSig( instSource, templateFromParam, iFrom) - newTypeSpec.from[finalSource] = template.from[source]( + 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 { - newTypeSpec.from[instSource] = template.from[source] + 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/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 04948df..00faddb 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 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/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 53e646f..d8df045 100644 --- a/src/pocomath.mjs +++ b/src/pocomath.mjs @@ -4,18 +4,10 @@ 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' -// Most of tuple is not ready yet: -const tupleReady = { - Tuple: tuple.Tuple, - equal: tuple.equal, - isZero: tuple.isZero, - length: tuple.length, - tuple: tuple.tuple -} import * as generic from './generic/all.mjs' import * as ops from './ops/all.mjs' const math = PocomathInstance.merge( - 'math', numbers, bigints, complex, tupleReady, 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 index 7b4ed35..0c6c0ae 100644 --- a/src/tuple/Types/Tuple.mjs +++ b/src/tuple/Types/Tuple.mjs @@ -23,7 +23,7 @@ Tuple.installType('Tuple', { from: { 'Tuple': convert => tu => ({elts: tu.elts.map(convert)}), // Here since there is no U it's a straight conversion: - T: t => ({elts: [u]}), // singleton promotion + 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. @@ -32,48 +32,48 @@ Tuple.installType('Tuple', { }) Tuple.promoteUnary = { - 'Tuple': ({'self(T)': me}) => t => ({elts: t.elts.map(me)}) + 'Tuple': ({'self(T)': me, tuple}) => t => tuple(...(t.elts.map(me))) } Tuple.promoteBinaryUnary = { - 'Tuple,Tuple': ({'self(T,T)': meB, 'self(T)': meU}) => (s,t) => { + '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.append(meB(s.elts[i], t.elts[i])) - else results.append(meU(s.elts[i])) + 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.append(meU(t.elts[i])) + if (i < t.elts.length) result.push(meU(t.elts[i])) else break } - return {elts: result} + return tuple(...result) } } Tuple.promoteBinary = { - 'Tuple,Tuple': ({'self(T,T)': meB}) => (s,t) => { + '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.append(meB(s.elts[i], t.elts[i])) + result.push(meB(s.elts[i], t.elts[i])) } - return {elts: result} + return tuple(...result) } } Tuple.promoteBinaryStrict = { - 'Tuple,Tuple': ({'self(T,T)': meB}) => (s,t) => { + '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.append(meB(s.elts[i], t.elts[i])) + result.push(meB(s.elts[i], t.elts[i])) } - return {elts: result} + return tuple(...result) } } diff --git a/src/tuple/equal.mjs b/src/tuple/equal.mjs deleted file mode 100644 index 8186624..0000000 --- a/src/tuple/equal.mjs +++ /dev/null @@ -1,14 +0,0 @@ -export * from './Types/Tuple.mjs' - -export const equal = { - // Change this to a template implementation (or maybe add template - // implementation to handle matching types, and have mixed template-base - // method returning false to catch test of two tuples of different types. - 'Tuple,Tuple': ({self, length}) => (s,t) => { - if (length(s) !== length(t)) return false - for (let i = 0; i < length(s); ++i) { - if (!self(s.elts[i], t.elts[i])) return false - } - return true - } -} 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/native.mjs b/src/tuple/native.mjs index fcf6f0d..66ba8f0 100644 --- a/src/tuple/native.mjs +++ b/src/tuple/native.mjs @@ -3,8 +3,8 @@ import {Tuple} from './Types/Tuple.mjs' export const add = Tuple.promoteBinaryUnary export const complex = Tuple.promoteBinaryStrict export const conjugate = Tuple.promoteUnary -// May want to replace equal with compare based on lexicographic ordering? -export {equal} from './equal.mjs' +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' @@ -14,6 +14,7 @@ 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 diff --git a/test/custom.mjs b/test/custom.mjs index d232dd5..6a399b0 100644 --- a/test/custom.mjs +++ b/test/custom.mjs @@ -18,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) }) @@ -34,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", () => { @@ -130,12 +131,9 @@ describe('A custom instance', () => { inst.typeMerge(3, inst.complex(4.5,2.1)), 'Merge to Complex') // The following is the current behavior, since 3 converts to 3+0i - // and 3n converts to 3n+0ni, both of which are technically Complex. - // This will remain the case even with templated Complex, because - // both Complex and Complex will refine Complex (for the - // sake of catching new specializations). Not sure whether that will be - // OK or a problem that will have to be dealt with. - assert.strictEqual(inst.typeMerge(3, 3n), 'Merge to Complex') + // 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... diff --git a/test/tuple/_native.mjs b/test/tuple/_native.mjs index 7727ac9..2cf56d1 100644 --- a/test/tuple/_native.mjs +++ b/test/tuple/_native.mjs @@ -18,11 +18,97 @@ describe('tuple', () => { {elts: [math.complex(3), math.complex(2n), math.complex(5.2)]}) }) - it('can be tested for zero', () => { + 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)) }) })