From cc4d77f128ae29540716999177966264d4e3d80a Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Mon, 28 Apr 2025 14:29:08 -0700 Subject: [PATCH 01/12] feat: add Vector generic type --- src/nanomath.js | 10 ++++--- src/vector/Vector.js | 43 ++++++++++++++++++++++++++++++ src/vector/__test__/Vector.spec.js | 35 ++++++++++++++++++++++++ src/vector/all.js | 1 + 4 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 src/vector/Vector.js create mode 100644 src/vector/__test__/Vector.spec.js create mode 100644 src/vector/all.js diff --git a/src/nanomath.js b/src/nanomath.js index ddeeeba..b95aff5 100644 --- a/src/nanomath.js +++ b/src/nanomath.js @@ -1,8 +1,9 @@ -import * as booleans from './boolean/all.js' import * as coretypes from './coretypes/all.js' +import * as booleans from './boolean/all.js' +import * as complex from './complex/all.js' import * as generics from './generic/all.js' import * as numbers from './number/all.js' -import * as complex from './complex/all.js' +import * as vectors from './vector/all.js' import {TypeDispatcher} from '#core/TypeDispatcher.js' // At the moment, since we are not sorting patterns in any way, @@ -12,9 +13,10 @@ import {TypeDispatcher} from '#core/TypeDispatcher.js' // whatever has been merged before.) // Hence, in building the math instance, we put generics first because // they should only kick in when there are not specific implementations, -// and complex next becausewe want its conversion (which converts _any_ +// and complex next because we want its conversion (which converts _any_ // non-complex type to complex, potentially making a poor overload choice) // to be tried last. -const math = new TypeDispatcher(generics, complex, booleans, coretypes, numbers) +const math = new TypeDispatcher( + generics, complex, vectors, booleans, coretypes, numbers) export default math diff --git a/src/vector/Vector.js b/src/vector/Vector.js new file mode 100644 index 0000000..0f82e4f --- /dev/null +++ b/src/vector/Vector.js @@ -0,0 +1,43 @@ +import {NotAType, Type} from '#core/Type.js' + +const isVector = v => Array.isArray(v) + +export const Vector = new Type(isVector, { + specialize(CompType) { + const compTest = CompType.test + const specTest = v => isVector(v) && v.every(compTest) + const typeName = `Vector(${CompType})` + if (CompType.concrete) { + const typeOptions = {typeName} + if ('zero' in CompType) typeOptions.zero = [CompType.zero] + const vectorCompType = new Type(specTest, typeOptions) + vectorCompType.Component = CompType + vectorCompType.vector = true + return vectorCompType + } + // Wrapping a generic type in Vector + return new Type(specTest, { + typeName, + specialize: (...args) => this.specialize(CompType.specialize(...args)), + specializesTo: VT => this.specializesTo(VT) + && CompType.specializesTo(VT.Component), + refine: (v, typer) => { + const eltTypes = v.map(elt => CompType.refine(elt, typer)) + const newCompType = eltTypes[0] + if (eltTypes.some(T => T !== newCompType)) { + throw new TypeError( + `can't refine ${typeName} to ${v}; ` + + `not all entries have type ${newCompType}`) + } + return this.specialize(newCompType) + } + }) + }, + specializesTo: VT => VT.vector, + refine(v, typer) { + const eltTypes = v.map(elt => typer(elt)) + let compType = eltTypes[0] + if (eltTypes.some(T => T !== compType)) compType = NotAType + return this.specialize(compType) + } +}) diff --git a/src/vector/__test__/Vector.spec.js b/src/vector/__test__/Vector.spec.js new file mode 100644 index 0000000..5233bb5 --- /dev/null +++ b/src/vector/__test__/Vector.spec.js @@ -0,0 +1,35 @@ +import assert from 'assert' +import {Vector} from '../Vector.js' +import {NotAType} from '#core/Type.js' +import math from '#nanomath' + +describe('Vector Type', () => { + it('correctly recognizes vectors', () => { + assert(Vector.test([3, 4])) + assert(!Vector.test({re: 3, im: 4})) + }) + it('can fully type vectors', () => { + const {BooleanT, Complex, NumberT} = math.types + const typ = math.typeOf + const cplx = math.complex + assert.strictEqual(typ([3, 4]), Vector(NumberT)) + assert.strictEqual(typ([true, false]), Vector(BooleanT)) + assert.strictEqual(typ([3, false]), Vector(NotAType)) + assert.strictEqual(typ([cplx(3, 4), cplx(5)]), Vector(Complex(NumberT))) + assert.strictEqual(typ([[3, 4], [5]]), Vector(Vector(NumberT))) + }) + it('can refine when nested', () => { + const {Complex, NumberT} = math.types + const cplx = math.complex + const VecComplex = Vector(Complex) + const vcEx = [cplx(3, 4), cplx(5)] + assert(VecComplex.test(vcEx)) + const vcType = VecComplex.refine(vcEx, math.typeOf) + assert.strictEqual(vcType, Vector(Complex(NumberT))) + const CplxVec = Complex(Vector) + const cvEx = cplx([3,4], [5, 0]) + assert(CplxVec.test(cvEx)) + const cvType = CplxVec.refine(cvEx, math.typeOf) + assert.strictEqual(cvType, Complex(Vector(NumberT))) + }) +}) diff --git a/src/vector/all.js b/src/vector/all.js new file mode 100644 index 0000000..b5fe798 --- /dev/null +++ b/src/vector/all.js @@ -0,0 +1 @@ +export * as typeDefinition from './Vector.js' -- 2.43.0 From 7d150e28608123647d0bdcf8f0cffcff13c7edd5 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Mon, 28 Apr 2025 16:02:26 -0700 Subject: [PATCH 02/12] feat: Add vector constructor/converter --- src/vector/Vector.js | 5 +++++ src/vector/__test__/type.js | 16 ++++++++++++++++ src/vector/all.js | 2 ++ src/vector/type.js | 12 ++++++++++++ 4 files changed, 35 insertions(+) create mode 100644 src/vector/__test__/type.js create mode 100644 src/vector/type.js diff --git a/src/vector/Vector.js b/src/vector/Vector.js index 0f82e4f..1fbf3f2 100644 --- a/src/vector/Vector.js +++ b/src/vector/Vector.js @@ -22,6 +22,10 @@ export const Vector = new Type(isVector, { specializesTo: VT => this.specializesTo(VT) && CompType.specializesTo(VT.Component), refine: (v, typer) => { + if (!v.length) { + throw new RangeError( + `no way to refine ${typeName} on an empty vector`) + } const eltTypes = v.map(elt => CompType.refine(elt, typer)) const newCompType = eltTypes[0] if (eltTypes.some(T => T !== newCompType)) { @@ -35,6 +39,7 @@ export const Vector = new Type(isVector, { }, specializesTo: VT => VT.vector, refine(v, typer) { + if (!v.length) return this.specialize(NotAType) // what else could we do? const eltTypes = v.map(elt => typer(elt)) let compType = eltTypes[0] if (eltTypes.some(T => T !== compType)) compType = NotAType diff --git a/src/vector/__test__/type.js b/src/vector/__test__/type.js new file mode 100644 index 0000000..43dc101 --- /dev/null +++ b/src/vector/__test__/type.js @@ -0,0 +1,16 @@ +import assert from 'assert' +import {NotAType} from '#core/Type.js' +import math from '#nanomath' + +describe('Vector type functions', () => { + it('can construct a vector', () => { + const vec = math.vector + const {BooleanT, NumberT, Vector} = math.types + assert.deepStrictEqual(vec(3, 4, 5), [3, 4, 5]) + assert.strictEqual( + vec.resolve([NumberT, NumberT, NumberT]).returns, Vector(NumberT)) + assert.deepStrictEqual(vec(3, true), [3, true]) + assert.strictEqual( + vec.resolve([NumberT, BooleanT]).returns, Vector(NotAType)) + }) +}) diff --git a/src/vector/all.js b/src/vector/all.js index b5fe798..81a3f9d 100644 --- a/src/vector/all.js +++ b/src/vector/all.js @@ -1 +1,3 @@ export * as typeDefinition from './Vector.js' +export * as type from './type.js' + diff --git a/src/vector/type.js b/src/vector/type.js new file mode 100644 index 0000000..d826372 --- /dev/null +++ b/src/vector/type.js @@ -0,0 +1,12 @@ +import {Vector} from './Vector.js' +import {NotAType, Returns} from '#core/Type.js' +import {Any, Multiple, match} from '#core/TypePatterns.js' + +export const vector = match(Multiple(Any), (math, TV) => { + if (!TV.length) return Returns(Vector(NotAType), () => []) + let CompType = TV[0] + if (TV.some(T => T !== CompType)) CompType = NotAType + return Returns(Vector(CompType), v => v) +}) + + -- 2.43.0 From f0023105811dac5a531fa3fbac48556c9ef11ad0 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Mon, 28 Apr 2025 16:25:03 -0700 Subject: [PATCH 03/12] feat: Add vector clone --- src/vector/__test__/utils.spec.js | 24 ++++++++++++++++++++++++ src/vector/all.js | 1 + src/vector/helpers.js | 15 +++++++++++++++ src/vector/utils.js | 3 +++ 4 files changed, 43 insertions(+) create mode 100644 src/vector/__test__/utils.spec.js create mode 100644 src/vector/helpers.js create mode 100644 src/vector/utils.js diff --git a/src/vector/__test__/utils.spec.js b/src/vector/__test__/utils.spec.js new file mode 100644 index 0000000..0e1a56e --- /dev/null +++ b/src/vector/__test__/utils.spec.js @@ -0,0 +1,24 @@ +import assert from 'assert' +import math from '#nanomath' + +describe('Vector utility functions', () => { + it('can clone a vector', () => { + const subj1 = [3, 4] + const clone1 = math.clone(subj1) + assert.deepStrictEqual(clone1, subj1) + assert(clone1 !== subj1) + const subj2 = [3, false] + const clone2 = math.clone(subj2) + assert.deepStrictEqual(clone2, subj2) + assert(clone2 !== subj2) + const subj3 = [[3, 4], [2, 5]] + const clone3 = math.clone(subj3) + assert.deepStrictEqual(clone3, subj3) + assert(clone3 !== subj3) + assert(clone3[0] !== subj3[0]) + assert(clone3[1] !== subj3[1]) + }) +}) + + + diff --git a/src/vector/all.js b/src/vector/all.js index 81a3f9d..cd390fa 100644 --- a/src/vector/all.js +++ b/src/vector/all.js @@ -1,3 +1,4 @@ export * as typeDefinition from './Vector.js' export * as type from './type.js' +export * as utilities from './utils.js' diff --git a/src/vector/helpers.js b/src/vector/helpers.js new file mode 100644 index 0000000..2b52280 --- /dev/null +++ b/src/vector/helpers.js @@ -0,0 +1,15 @@ +import {Vector} from './Vector.js' +import {NotAType, Returns} from '#core/Type.js' +import {match} from '#core/TypePatterns.js' + +export const promoteUnary = name => match(Vector, (math, V, strategy) => { + if (V.Component === NotAType) { + // have to resolve element by element :-( + return Returns(V, v => v.map( + elt => math.resolve(name, math.typeOf(elt), strategy)(elt))) + } + const compOp = math.resolve(name, V.Component, strategy) + return Returns(Vector(compOp.returns), v => v.map(elt => compOp(elt))) +}) + + diff --git a/src/vector/utils.js b/src/vector/utils.js new file mode 100644 index 0000000..c717a96 --- /dev/null +++ b/src/vector/utils.js @@ -0,0 +1,3 @@ +import {promoteUnary} from './helpers.js' + +export const clone = promoteUnary('clone') -- 2.43.0 From 3b882f3d531c2c1d5ab686cd0dd24919a2760b1b Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Mon, 28 Apr 2025 19:32:12 -0700 Subject: [PATCH 04/12] feat: extend all utils to vectors elementwise & fix parameter match grouping --- src/core/TypePatterns.js | 7 ++++--- src/core/__test__/TypePatterns.spec.js | 4 ++++ src/generic/utils.js | 2 +- src/vector/__test__/utils.spec.js | 17 +++++++++++++++++ src/vector/utils.js | 7 +++++++ 5 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/core/TypePatterns.js b/src/core/TypePatterns.js index 0705894..b3cb566 100644 --- a/src/core/TypePatterns.js +++ b/src/core/TypePatterns.js @@ -51,7 +51,8 @@ class SequencePattern extends TypePattern { const [newPos, newMatch] = pat.match(typeSequence, options) if (newPos < 0) return [-1, Undefined] options.position = newPos - matches.push(newMatch) + if (Array.isArray(newMatch)) matches.push(...newMatch) + else matches.push(newMatch) } return [options.position, matches] } @@ -125,7 +126,7 @@ class OptionalPattern extends TypePattern { options.position = newPos matches.push(newMatch) } - return [options.position, matches] + return [options.position, [matches]] } sampleTypes() {return []} equal(other) { @@ -148,7 +149,7 @@ class MultiPattern extends TypePattern { const matches = [] while (true) { const [newPos, newMatch] = this.pattern.match(typeSequence, options) - if (newPos < 0) return [options.position, matches] + if (newPos < 0) return [options.position, [matches]] options.position = newPos matches.push(newMatch) } diff --git a/src/core/__test__/TypePatterns.spec.js b/src/core/__test__/TypePatterns.spec.js index 5aefc3a..b2750be 100644 --- a/src/core/__test__/TypePatterns.spec.js +++ b/src/core/__test__/TypePatterns.spec.js @@ -53,6 +53,10 @@ describe('Type patterns', () => { midOpt.match([Undefined, TypeOfTypes, TypeOfTypes]), [-1, Undefined]) assert.deepStrictEqual(midOpt.sampleTypes(), [Undefined, Undefined]) + const justMulti = pattern(Multiple(Undefined)) + assert.deepStrictEqual( + justMulti.match([Undefined, Undefined]), + [2, [[Undefined, Undefined]]]) const midMulti = pattern([Undefined, Multiple(TypeOfTypes), Undefined]) assert.deepStrictEqual( midMulti.match([Undefined, Undefined]), diff --git a/src/generic/utils.js b/src/generic/utils.js index 716609e..9a17f52 100644 --- a/src/generic/utils.js +++ b/src/generic/utils.js @@ -18,7 +18,7 @@ export const isZero = match(Passthru, (math, [T]) => { const eq = math.equal.resolve([T, T]) return ReturnsAs(eq, x => eq(z, x)) }) -export const nonImaginary = match(Passthru, boolnum(() => true)) +export const nonImaginary = match(Any, boolnum(() => true)) export const re = match(Any, (_math, T) => Returns(T, t => t)) export const im = match(Any, (math, T) => { const z = math.zero(T) diff --git a/src/vector/__test__/utils.spec.js b/src/vector/__test__/utils.spec.js index 0e1a56e..eb1f0cc 100644 --- a/src/vector/__test__/utils.spec.js +++ b/src/vector/__test__/utils.spec.js @@ -18,6 +18,23 @@ describe('Vector utility functions', () => { assert(clone3[0] !== subj3[0]) assert(clone3[1] !== subj3[1]) }) + it('performs utilities elementwise', () => { + const cplx = math.complex + const sink = math.vector( + NaN, -Infinity, 7, + cplx(3, 4), cplx(3.5, -2.5), cplx(2.2), cplx(cplx(3, 4)) + ) + const t = true, f = false + assert.deepStrictEqual(math.isnan(sink), [t, f, f, f, f, f, f]) + assert.deepStrictEqual(math.isfinite(sink), [f, f, t, t, t, t, t]) + assert.deepStrictEqual(math.isInteger(sink), [f, f, t, t, f, f, t]) + assert.deepStrictEqual(math.isReal(sink), [t, t, t, f, f, t, f]) + assert.deepStrictEqual(math.nonImaginary(sink), [t, t, t, f, f, t, t]) + assert.deepStrictEqual(math.re(sink), [NaN, -Infinity, 7, 3, 3.5, 2.2, 3]) + assert.deepStrictEqual( + math.im(sink), + [0, 0, 0, cplx(0, 4), cplx(0, -2.5), cplx(0, 0), cplx(cplx(0, 4))]) + }) }) diff --git a/src/vector/utils.js b/src/vector/utils.js index c717a96..54c6fa9 100644 --- a/src/vector/utils.js +++ b/src/vector/utils.js @@ -1,3 +1,10 @@ import {promoteUnary} from './helpers.js' export const clone = promoteUnary('clone') +export const isnan = promoteUnary('isnan') +export const isfinite = promoteUnary('isfinite') +export const isInteger = promoteUnary('isInteger') +export const isReal = promoteUnary('isReal') +export const nonImaginary = promoteUnary('nonImaginary') +export const re = promoteUnary('re') +export const im = promoteUnary('im') -- 2.43.0 From 7b799f71839b896fcbafd21e428cd2d675724184 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Mon, 28 Apr 2025 21:25:02 -0700 Subject: [PATCH 05/12] feat: Add vector deepEqual --- src/generic/relational.js | 3 +++ src/vector/__test__/relational.spec.js | 16 +++++++++++++++ src/vector/__test__/{type.js => type.spec.js} | 0 src/vector/all.js | 1 + src/vector/relational.js | 20 +++++++++++++++++++ src/vector/type.js | 2 +- 6 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 src/vector/__test__/relational.spec.js rename src/vector/__test__/{type.js => type.spec.js} (100%) create mode 100644 src/vector/relational.js diff --git a/src/generic/relational.js b/src/generic/relational.js index 2d70a1e..0455ddb 100644 --- a/src/generic/relational.js +++ b/src/generic/relational.js @@ -39,6 +39,9 @@ export const equal = match([Any, Any], (math, [T, U]) => { // now that we have `equal` and `exceeds`, pretty much everything else should // be easy: +export const deepEqual = match( + Passthru, (math, types, strategy) => math.equal.resolve(types, strategy)) + export const compare = match([Any, Any], (math, [T, U]) => { const eq = math.equal.resolve([T, U]) const gt = math.exceeds.resolve([T, U]) diff --git a/src/vector/__test__/relational.spec.js b/src/vector/__test__/relational.spec.js new file mode 100644 index 0000000..308ec80 --- /dev/null +++ b/src/vector/__test__/relational.spec.js @@ -0,0 +1,16 @@ +import assert from 'assert' +import math from '#nanomath' + +describe('Vector relational functions', () => { + it('can test two vectors for deep equality', () => { + const dEq = math.deepEqual + const pyth = [3, 4, 5] + assert.strictEqual(dEq(pyth, [3, 4, 5]), true) + assert.strictEqual(dEq(pyth, [3, 4, 6]), false) + assert.strictEqual(dEq(pyth, [3, 4, [5]]), false) + assert.strictEqual(dEq(3, 3 + 1e-13), true) + assert.strictEqual(dEq(pyth, [3+1e-13, 4-1e-13, 5]), true) + assert.strictEqual(dEq([pyth, pyth], [pyth, [3, 4, 5]]), true) + assert.strictEqual(dEq([3], 3), false) + }) +}) diff --git a/src/vector/__test__/type.js b/src/vector/__test__/type.spec.js similarity index 100% rename from src/vector/__test__/type.js rename to src/vector/__test__/type.spec.js diff --git a/src/vector/all.js b/src/vector/all.js index cd390fa..374601f 100644 --- a/src/vector/all.js +++ b/src/vector/all.js @@ -1,4 +1,5 @@ export * as typeDefinition from './Vector.js' +export * as relational from './relational.js' export * as type from './type.js' export * as utilities from './utils.js' diff --git a/src/vector/relational.js b/src/vector/relational.js new file mode 100644 index 0000000..11272ed --- /dev/null +++ b/src/vector/relational.js @@ -0,0 +1,20 @@ +import {Vector} from './Vector.js' +import {NotAType, Returns} from '#core/Type.js' +import {Any, match} from '#core/TypePatterns.js' +import {BooleanT} from '#boolean/BooleanT.js' + +export const deepEqual = [ + match([Vector, Any], Returns(BooleanT, () => false)), + match([Any, Vector], Returns(BooleanT, () => false)), + match([Vector, Vector], (math, [V, W]) => { + if (V.Component === NotAType || W.Component === NotAType) { + return Returns(BooleanT, (v, w) => v === w + || (v.length === w.length + && v.every((e, i) => math.deepEqual(e, w[i])))) + } + const compDeep = math.deepEqual.resolve([V.Component, W.Component]) + return Returns(BooleanT, (v,w) => v === w + || (v.length === w.length + && v.every((e, i) => compDeep(e, w[i])))) + }) +] diff --git a/src/vector/type.js b/src/vector/type.js index d826372..bad1b64 100644 --- a/src/vector/type.js +++ b/src/vector/type.js @@ -2,7 +2,7 @@ import {Vector} from './Vector.js' import {NotAType, Returns} from '#core/Type.js' import {Any, Multiple, match} from '#core/TypePatterns.js' -export const vector = match(Multiple(Any), (math, TV) => { +export const vector = match(Multiple(Any), (math, [TV]) => { if (!TV.length) return Returns(Vector(NotAType), () => []) let CompType = TV[0] if (TV.some(T => T !== CompType)) CompType = NotAType -- 2.43.0 From c3c2bbbf780019844410af20a0d54c4cf189ebc4 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Thu, 1 May 2025 20:00:24 -0700 Subject: [PATCH 06/12] feat: equality on Vector elementwise --- src/coretypes/relational.js | 12 +++-- src/vector/__test__/relational.spec.js | 32 ++++++++--- src/vector/relational.js | 75 +++++++++++++++++++++++++- 3 files changed, 107 insertions(+), 12 deletions(-) diff --git a/src/coretypes/relational.js b/src/coretypes/relational.js index a45cadf..896d7c8 100644 --- a/src/coretypes/relational.js +++ b/src/coretypes/relational.js @@ -1,8 +1,14 @@ import {TypeOfTypes, Undefined} from '#core/Type.js' -import {match} from '#core/TypePatterns.js' +import {Any, Multiple, match} from '#core/TypePatterns.js' import {boolnum} from '#number/helpers.js' export const indistinguishable = [ - match([Undefined, Undefined], boolnum(() => true)), - match([TypeOfTypes, TypeOfTypes], boolnum((t, u) => t === u)) + // I don't think there's any other type that should be indistinguishable + // from undefined: + match([Undefined, Any, Multiple(Any)], boolnum(() => false)), + match([Any, Undefined, Multiple(Any)], boolnum(() => false)), + match([Undefined, Undefined, Multiple(Any)], boolnum(() => true)), + match( + [TypeOfTypes, TypeOfTypes, Multiple(Any)], + boolnum((t, u) => t === u)) ] diff --git a/src/vector/__test__/relational.spec.js b/src/vector/__test__/relational.spec.js index 308ec80..3637b86 100644 --- a/src/vector/__test__/relational.spec.js +++ b/src/vector/__test__/relational.spec.js @@ -1,16 +1,34 @@ import assert from 'assert' import math from '#nanomath' +const t = true +const f = false describe('Vector relational functions', () => { it('can test two vectors for deep equality', () => { const dEq = math.deepEqual const pyth = [3, 4, 5] - assert.strictEqual(dEq(pyth, [3, 4, 5]), true) - assert.strictEqual(dEq(pyth, [3, 4, 6]), false) - assert.strictEqual(dEq(pyth, [3, 4, [5]]), false) - assert.strictEqual(dEq(3, 3 + 1e-13), true) - assert.strictEqual(dEq(pyth, [3+1e-13, 4-1e-13, 5]), true) - assert.strictEqual(dEq([pyth, pyth], [pyth, [3, 4, 5]]), true) - assert.strictEqual(dEq([3], 3), false) + assert.strictEqual(dEq(pyth, [3, 4, 5]), t) + assert.strictEqual(dEq(pyth, [3, 4, 6]), f) + assert.strictEqual(dEq(pyth, [3, 4, [5]]), f) + assert.strictEqual(dEq(3, 3 + 1e-13), t) + assert.strictEqual(dEq(pyth, [3+1e-13, 4-1e-13, 5]), t) + assert.strictEqual(dEq([pyth, pyth], [pyth, [3, 4, 5]]), t) + assert.strictEqual(dEq([3], 3), f) + }) + it('performs equal elementwise', () => { + const eq = math.equal + const pyth = [3, 4, 5] + assert.deepStrictEqual(eq(pyth, 4), [f, t, f]) + assert.deepStrictEqual(eq(5, pyth), [f, f, t]) + assert.deepStrictEqual(eq(pyth, pyth), [t, t, t]) + assert.deepStrictEqual(eq(pyth, [3]), [t, f, f]) + assert.deepStrictEqual(eq([4 + 1e-14], pyth), [f, t, f]) + assert.deepStrictEqual(eq(pyth, [3, 4]), [t, t, f]) + assert.deepStrictEqual(eq([3, 2], pyth), [t, f, f]) + assert.deepStrictEqual(eq([pyth, pyth], [3, 4]), [[t, f, f], [f, t, f]]) + assert.deepStrictEqual( + eq([pyth, pyth], [[5], pyth]), [[f, f, t], [t, t, t]]) + assert.deepStrictEqual( + eq([[1, 2], [3, 4]], [[1, 3], [2, 4]]), [[t, f], [f, t]]) }) }) diff --git a/src/vector/relational.js b/src/vector/relational.js index 11272ed..23e5835 100644 --- a/src/vector/relational.js +++ b/src/vector/relational.js @@ -1,6 +1,6 @@ import {Vector} from './Vector.js' -import {NotAType, Returns} from '#core/Type.js' -import {Any, match} from '#core/TypePatterns.js' +import {NotAType, Returns, Undefined} from '#core/Type.js' +import {Any, Optional, match} from '#core/TypePatterns.js' import {BooleanT} from '#boolean/BooleanT.js' export const deepEqual = [ @@ -18,3 +18,74 @@ export const deepEqual = [ && v.every((e, i) => compDeep(e, w[i])))) }) ] + +export const indistinguishable = [ + match([Vector, Any, Optional([Any, Any])], (math, [V, E, T]) => { + const VComp = V.Component + if (T.length === 0) { // no tolerances + const same = math.indistinguishable.resolve([VComp, E]) + return Returns( + Vector(same.returns), (v, e) => v.map(f => same(f, e))) + } + const [[RT, AT]] = T + const same = math.indistinguishable.resolve([VComp, E, RT, AT]) + return Returns( + Vector(same.returns), + (v, e, [[rT, aT]]) => v.map(f => same(f, e, rT, aT))) + }), + match([Any, Vector, Optional([Any, Any])], (math, [E, V, T]) => { + // reimplement to get other order in same so as not to assume + // same is symmetric, even though it probably is + const VComp = V.Component + if (T.length === 0) { // no tolerances + const same = math.indistinguishable.resolve([E, VComp]) + return Returns( + Vector(same.returns), (e, v) => v.map(f => same(e, f))) + } + const [[RT, AT]] = T + const same = math.indistinguishable.resolve([E, VComp, RT, AT]) + return Returns( + Vector(same.returns), + (e, v, [[rT, aT]]) => v.map(f => same(e, f, rT, aT))) + }), + match([Vector, Vector, Optional([Any, Any])], (math, [V, W, T]) => { + const VComp = V.Component + const WComp = W.Component + let same + let sameNoV + let sameNoW + if (T.length === 0) { // no tolerances + same = math.indistinguishable.resolve([VComp, WComp]) + sameNoV = math.indistinguishable.resolve([Undefined, WComp]) + sameNoW = math.indistinguishable.resolve([VComp, Undefined]) + } else { + const [[RT, AT]] = T + same = math.indistinguishable.resolve([VComp, WComp, RT, AT]) + sameNoV = math.indistinguishable.resolve([Undefined, WComp, RT, AT]) + sameNoW = math.indistinguishable.resolve([VComp, Undefined, RT, AT]) + } + return Returns( + Vector(same.returns), + (v, w, [tol = [0, 0]]) => { + const [rT, aT] = tol + const vInc = Number(v.length > 1) + const wInc = Number(w.length >= v.length || w.length > 1) + const retval = [] + let vIx = 0 + let wIx = 0 + let remainder = vIx < v.length || wIx < w.length + while ((vInc && vIx < v.length) + || (wInc && wIx < w.length) + ) { + if (vIx >= v.length) { + retval.push(sameNoV(undefined, w[wIx], rT, aT)) + } else if (wIx >= w.length) { + retval.push(sameNoW(v[vIx], undefined, rT, aT)) + } else retval.push(same(v[vIx], w[wIx], rT, aT)) + vIx += vInc + wIx += wInc + } + return retval + }) + }) +] -- 2.43.0 From edfba089e3b07c7b1fee1949a77b7dcb8b012cfa Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Fri, 2 May 2025 19:03:54 -0700 Subject: [PATCH 07/12] feat: Complete relational functions for vectors Toward its goal, this commit also: * Adds a new section of logical functions, and defines `not`, `and`, `or` for all current types. * Extends `OneOf` choice/union type to allow argument types that are themselves `OneOf` types. * Adds a readable .toString() method for TypePatterns. * Defines negate (as a no-op) and isnan (as always true) for the Undefined type * Extends comparisons to the Undefined type (to handle comparing vectors of different lengths) --- src/boolean/all.js | 1 + src/boolean/logical.js | 9 ++++ src/core/Type.js | 17 ++++-- src/core/TypeDispatcher.js | 9 +++- src/core/TypePatterns.js | 8 +++ src/coretypes/all.js | 1 + src/coretypes/arithmetic.js | 6 +++ src/generic/__test__/relational.spec.js | 2 +- src/generic/all.js | 1 + src/generic/logical.js | 40 ++++++++++++++ src/generic/relational.js | 19 ++++--- src/number/all.js | 1 + src/number/logical.js | 6 +++ src/number/relational.js | 11 +++- src/vector/__test__/relational.spec.js | 13 +++++ src/vector/all.js | 1 + src/vector/helpers.js | 70 +++++++++++++++++++++++-- src/vector/logical.js | 7 +++ src/vector/relational.js | 35 +++++++++++-- 19 files changed, 238 insertions(+), 19 deletions(-) create mode 100644 src/boolean/logical.js create mode 100644 src/coretypes/arithmetic.js create mode 100644 src/generic/logical.js create mode 100644 src/number/logical.js create mode 100644 src/vector/logical.js diff --git a/src/boolean/all.js b/src/boolean/all.js index be70fad..1711e64 100644 --- a/src/boolean/all.js +++ b/src/boolean/all.js @@ -1,3 +1,4 @@ export * as typeDefinition from './BooleanT.js' +export * as logical from './logical.js' export * as type from './type.js' export * as utilities from './utils.js' diff --git a/src/boolean/logical.js b/src/boolean/logical.js new file mode 100644 index 0000000..2148018 --- /dev/null +++ b/src/boolean/logical.js @@ -0,0 +1,9 @@ +import {BooleanT} from './BooleanT.js' +import {Returns} from '#core/Type.js' +import {match} from '#core/TypePatterns.js' + +export const not = match(BooleanT, Returns(BooleanT, p => !p)) +export const and = match( + [BooleanT, BooleanT], Returns(BooleanT, (p, q) => p && q)) +export const or = match( + [BooleanT, BooleanT], Returns(BooleanT, (p, q) => p || q)) diff --git a/src/core/Type.js b/src/core/Type.js index 398aaaa..daa8ca7 100644 --- a/src/core/Type.js +++ b/src/core/Type.js @@ -71,21 +71,32 @@ NotAType._doNotMerge = true const unionDirectory = new ArrayKeyedMap() // make sure only one of each union export const OneOf = (...types) => { + if (!types.length) { + throw new RangeError('cannot choose OneOf no types at all') + } const nonType = types.findIndex(T => !(T instanceof Type)) if (nonType >= 0) { throw new RangeError( `OneOf can only take type arguments, not ${types[nonType]}`) } - const typeSet = new Set(types) // remove duplicates + const typeSet = new Set() // remove duplicates: + for (const type of types) { + if (type.unifies) { + type.unifies.forEach(t => typeSet.add(t)) + } else typeSet.add(type) + } + if (typeSet.size === 1) return types[0] const typeList = Array.from(typeSet).sort() // canonical order const generic = typeList.find(T => !T.concrete) if (generic) { throw new RangeError(`OneOf can only take concrete types, not ${generic}`) } if (!unionDirectory.has(typeList)) { - unionDirectory.set(typeList, new Type( + const unionType = new Type( t => typeList.some(T => T.test(t)), - {typeName: typeList.join('|')})) + {typeName: typeList.join('|')}) + unionType.unifies = typeList + unionDirectory.set(typeList, unionType) } return unionDirectory.get(typeList) } diff --git a/src/core/TypeDispatcher.js b/src/core/TypeDispatcher.js index c56e39d..941d888 100644 --- a/src/core/TypeDispatcher.js +++ b/src/core/TypeDispatcher.js @@ -309,9 +309,9 @@ export class TypeDispatcher { let needItem = true let item let template + let pattern if (imps.length) { for (const options of [{}, {convert: true}]) { - let pattern for ([pattern, item] of imps) { let finalIndex ;[finalIndex, template] = pattern.match(types, options) @@ -327,11 +327,18 @@ export class TypeDispatcher { needItem = false item = this._fallbacks[key] template = types + pattern = Passthru } if (needItem) { throw new ResolutionError( `no matching definition of '${key}' on '${types}'`) } + /* The following message is often helpful in debugging, so left it + in but commented out; likely at some point we should have some + sort of trace facility that this would be a part of: + */ + // console.log(`RESOLVING ${key} on ${types} matches ${pattern}`) + // If this key is producing a non-function value, we're done if (!isPlainFunction(item)) { behave.set(bhvix, item) diff --git a/src/core/TypePatterns.js b/src/core/TypePatterns.js index b3cb566..a67a3b9 100644 --- a/src/core/TypePatterns.js +++ b/src/core/TypePatterns.js @@ -9,6 +9,7 @@ export class TypePattern { throw new Error('Specific TypePatterns must implement sampleTypes') } equal(other) {return other.constructor === this.constructor} + toString() {return 'Abstract Pattern (?!)'} } class MatchTypePattern extends TypePattern { @@ -34,6 +35,7 @@ class MatchTypePattern extends TypePattern { } sampleTypes() {return [this.type]} equal(other) {return super.equal(other) && this.type === other.type} + toString() {return `Match(${this.type})`} } class SequencePattern extends TypePattern { @@ -64,6 +66,7 @@ class SequencePattern extends TypePattern { && this.patterns.length === other.patterns.length && this.patterns.every((elt, ix) => elt.equal(other.patterns[ix])) } + toString() {return `[${this.patterns}]`} } class PredicatePattern extends TypePattern { @@ -84,6 +87,7 @@ class PredicatePattern extends TypePattern { equal(other) { return super.equal(other) && this.predicate === other.predicate } + toString() {return `Test(${this.predicate})`} } export const pattern = patternOrSpec => { @@ -106,6 +110,7 @@ class AnyPattern extends TypePattern { : [-1, Undefined] } sampleTypes() {return [Undefined]} + toString() {return 'Any'} } export const Any = new AnyPattern() @@ -132,6 +137,7 @@ class OptionalPattern extends TypePattern { equal(other) { return super.equal(other) && this.pattern.equal(other.pattern) } + toString() {return `?${this.pattern}`} } export const Optional = item => new OptionalPattern(item) @@ -158,6 +164,7 @@ class MultiPattern extends TypePattern { equal(other) { return super.equal(other) && this.pattern.equal(other.pattern) } + toString() {return `${this.pattern}*`} } export const Multiple = item => new MultiPattern(item) @@ -170,6 +177,7 @@ class PassthruPattern extends TypePattern { return [typeSequence.length, typeSequence.slice(position)] } sampleTypes() {return []} + toString() {return 'Passthru'} } export const Passthru = new PassthruPattern() diff --git a/src/coretypes/all.js b/src/coretypes/all.js index 18b23af..0f81e87 100644 --- a/src/coretypes/all.js +++ b/src/coretypes/all.js @@ -1,2 +1,3 @@ +export * from './arithmetic.js' export * from './relational.js' export * from './utils.js' diff --git a/src/coretypes/arithmetic.js b/src/coretypes/arithmetic.js new file mode 100644 index 0000000..1e2f279 --- /dev/null +++ b/src/coretypes/arithmetic.js @@ -0,0 +1,6 @@ +import {Returns, Undefined} from '#core/Type.js' +import {match} from '#core/TypePatterns.js' +import {boolnum} from '#number/helpers.js' + +export const isnan = match(Undefined, boolnum(() => true)) +export const negate = match(Undefined, Returns(Undefined, () => undefined)) diff --git a/src/generic/__test__/relational.spec.js b/src/generic/__test__/relational.spec.js index 18c2f95..b3ac1aa 100644 --- a/src/generic/__test__/relational.spec.js +++ b/src/generic/__test__/relational.spec.js @@ -48,7 +48,7 @@ describe('generic relational functions', () => { assert.throws(() => compare(false, NaN), TypeError) assert(isNaN(compare(NaN, NaN))) assert.throws(() => compare(true, false), TypeError) - assert.throws(() => compare(undefined, -1), ResolutionError) + assert.throws(() => compare(math.types.BooleanT, -1), ResolutionError) }) it('determines the sign of numeric values', () => { const {sign} = math diff --git a/src/generic/all.js b/src/generic/all.js index 89d8802..5560d5d 100644 --- a/src/generic/all.js +++ b/src/generic/all.js @@ -1,3 +1,4 @@ export * as arithmetic from './arithmetic.js' +export * as logical from './logical.js' export * as relational from './relational.js' export * as utilities from './utils.js' diff --git a/src/generic/logical.js b/src/generic/logical.js new file mode 100644 index 0000000..df90914 --- /dev/null +++ b/src/generic/logical.js @@ -0,0 +1,40 @@ +import {ReturnsAs} from './helpers.js' + +import {OneOf, Returns} from '#core/Type.js' +import {Any, Multiple, match} from '#core/TypePatterns.js' +import {boolnum} from '#number/helpers.js' + +export const not = match(Any, (math, T) => { + const bool = math.boolean.resolve(T) + return ReturnsAs(bool, t => !bool(t)) +}) + +export const and = [ + match([], boolnum(() => true)), + match(Any, (_math, T) => Returns(T, t => t)), + match([Any, Any], (math, [T, U]) => { + const bool = math.boolean.resolve(T) + return Returns(OneOf(T, U), (t, u) => bool(t) ? u : t) + }), + match([Any, Any, Any, Multiple(Any)], (math, [T, U, V, Rest]) => { + const andRest = math.and.resolve([U, V, ...Rest]) + const andFirst = math.and.resolve([T, andRest.returns]) + return ReturnsAs( + andFirst, (t, u, v, rest) => andFirst(t, andRest(u, v, ...rest))) + }) +] + +export const or = [ + match([], boolnum(() => false)), + match(Any, (_math, T) => Returns(T, t => t)), + match([Any, Any], (math, [T, U]) => { + const bool = math.boolean.resolve(T) + return Returns(OneOf(T, U), (t, u) => bool(t) ? t : u) + }), + match([Any, Any, Any, Multiple(Any)], (math, [T, U, V, Rest]) => { + const orRest = math.or.resolve([U, V, ...Rest]) + const orFirst = math.and.resolve([T, orRest.returns]) + return ReturnsAs( + orFirst, (t, u, v, rest) => orFirst(t, orRest(u, v, ...rest))) + }) +] diff --git a/src/generic/relational.js b/src/generic/relational.js index 0455ddb..15ce19a 100644 --- a/src/generic/relational.js +++ b/src/generic/relational.js @@ -82,35 +82,42 @@ export const sign = match(Any, (math, T) => { export const unequal = match(Passthru, (math, types) => { const eq = math.equal.resolve(types) - return ReturnsAs(eq, (...args) => !eq(...args)) + const not = math.not.resolve(eq.returns) + return ReturnsAs(not, (...args) => not(eq(...args))) }) export const larger = match([Any, Any], (math, [T, U]) => { const eq = math.equal.resolve([T, U]) const bigger = math.exceeds.resolve([T, U]) - return boolnum((t, u) => !eq(t, u) && bigger(t, u))(math) + const not = math.not.resolve(eq.returns) + const and = math.and.resolve([not.returns, bigger.returns]) + return ReturnsAs(and, (t, u) => and(not(eq(t, u)), bigger(t, u))) }) export const isPositive = match(Any, (math, T) => { const zero = math.zero(T) const larger = math.larger.resolve([T, T]) - return boolnum(t => larger(t, zero)) + return ReturnsAs(larger, t => larger(t, zero)) }) export const largerEq = match([Any, Any], (math, [T, U]) => { const eq = math.equal.resolve([T, U]) const bigger = math.exceeds.resolve([T, U]) - return ReturnsAs(bigger, (t, u) => eq(t, u) || bigger(t, u)) + const or = math.or.resolve([eq.returns, bigger.returns]) + return ReturnsAs(or, (t, u) => or(eq(t, u), bigger(t, u))) }) export const smaller = match([Any, Any], (math, [T, U]) => { const eq = math.equal.resolve([T, U]) const bigger = math.exceeds.resolve([U, T]) - return boolnum((t, u) => !eq(t, u) && bigger(u, t))(math) + const not = math.not.resolve(eq.returns) + const and = math.and.resolve([not.returns, bigger.returns]) + return ReturnsAs(and, (t, u) => and(not(eq(t, u)), bigger(u, t))) }) export const smallerEq = match([Any, Any], (math, [T, U]) => { const eq = math.equal.resolve([T, U]) const bigger = math.exceeds.resolve([U, T]) - return ReturnsAs(bigger, (t, u) => eq(t, u) || bigger(u, t)) + const or = math.or.resolve([eq.returns, bigger.returns]) + return ReturnsAs(or, (t, u) => or(eq(t, u), bigger(u, t))) }) diff --git a/src/number/all.js b/src/number/all.js index 7872d78..df37bac 100644 --- a/src/number/all.js +++ b/src/number/all.js @@ -1,5 +1,6 @@ export * as typeDefinition from './NumberT.js' export * as arithmetic from './arithmetic.js' +export * as logical from './logical.js' export * as relational from './relational.js' export * as type from './type.js' export * as utils from './utils.js' diff --git a/src/number/logical.js b/src/number/logical.js new file mode 100644 index 0000000..6d6311b --- /dev/null +++ b/src/number/logical.js @@ -0,0 +1,6 @@ +import {boolnum} from './helpers.js' +import {NumberT} from './NumberT.js' + +import {match} from '#core/TypePatterns.js' + +export const not = match(NumberT, boolnum(a => !a)) diff --git a/src/number/relational.js b/src/number/relational.js index 2d2e8df..c9d621d 100644 --- a/src/number/relational.js +++ b/src/number/relational.js @@ -1,7 +1,9 @@ -import {match, Optional} from '#core/TypePatterns.js' import {boolnum} from './helpers.js' import {NumberT} from './NumberT.js' +import {Undefined} from '#core/Type.js' +import {match, Optional} from '#core/TypePatterns.js' + // In nanomath, we take the point of view that two comparators are primitive: // indistinguishable(a, b, relTol, absTol), and exceeds(a, b). All others // are defined generically in terms of these. They typically return BooleanT, @@ -32,4 +34,9 @@ export const indistinguishable = match( // Returns truthy if a (interpreted as completely precise) represents a // greater value than b (interpreted as completely precise). Note that even if // so, a and b might be indistinguishable() to some tolerances. -export const exceeds = match([NumberT, NumberT], boolnum((a, b) => a > b)) +export const exceeds = [ + match([NumberT, NumberT], boolnum((a, b) => a > b)), + // Needed to allow comparison of vectors of different lengths: + match([Undefined, NumberT], boolnum(() => false)), + match([NumberT, Undefined], boolnum(() => false)) +] diff --git a/src/vector/__test__/relational.spec.js b/src/vector/__test__/relational.spec.js index 3637b86..a6e8871 100644 --- a/src/vector/__test__/relational.spec.js +++ b/src/vector/__test__/relational.spec.js @@ -31,4 +31,17 @@ describe('Vector relational functions', () => { assert.deepStrictEqual( eq([[1, 2], [3, 4]], [[1, 3], [2, 4]]), [[t, f], [f, t]]) }) + it('performs order comparisons elementwise', () => { + const pyth = [3, 4, 5] + const ans = [-1, 0, 1] + const powers = [2, 4, 8] + assert.deepStrictEqual(math.compare(pyth, [5, 4, 3]), ans) + assert.deepStrictEqual(math.sign([-Infinity, 0, 3]), ans) + assert.deepStrictEqual(math.unequal(pyth, [5, 4 + 1e-14, 5]), [t, f, f]) + assert.deepStrictEqual(math.larger(pyth, powers), [t, f, f]) + assert.deepStrictEqual(math.isPositive(ans), [f, f, t]) + assert.deepStrictEqual(math.largerEq(pyth, powers), [t, t, f]) + assert.deepStrictEqual(math.smaller(pyth, powers), [f, f, t]) + assert.deepStrictEqual(math.smallerEq(pyth, powers), [f, t, t]) + }) }) diff --git a/src/vector/all.js b/src/vector/all.js index 374601f..bd58b0b 100644 --- a/src/vector/all.js +++ b/src/vector/all.js @@ -1,4 +1,5 @@ export * as typeDefinition from './Vector.js' +export * as logical from './logical.js' export * as relational from './relational.js' export * as type from './type.js' export * as utilities from './utils.js' diff --git a/src/vector/helpers.js b/src/vector/helpers.js index 2b52280..06ada71 100644 --- a/src/vector/helpers.js +++ b/src/vector/helpers.js @@ -1,6 +1,6 @@ import {Vector} from './Vector.js' -import {NotAType, Returns} from '#core/Type.js' -import {match} from '#core/TypePatterns.js' +import {NotAType, Returns, Undefined} from '#core/Type.js' +import {Any, match} from '#core/TypePatterns.js' export const promoteUnary = name => match(Vector, (math, V, strategy) => { if (V.Component === NotAType) { @@ -12,4 +12,68 @@ export const promoteUnary = name => match(Vector, (math, V, strategy) => { return Returns(Vector(compOp.returns), v => v.map(elt => compOp(elt))) }) - +export const promoteBinary = name => [ + match([Vector, Any], (math, [V, E], strategy) => { + const VComp = V.Component + if (VComp === NotAType) { + return Returns(V, (v, e) => v.map( + f => math.resolve(name, [math.typeOf(f), E], strategy)(f, e))) + } + const compOp = math.resolve(name, [VComp, E], strategy) + return Returns( + Vector(compOp.returns), (v, e) => v.map(f => compOp(f, e))) + }), + match([Any, Vector], (math, [E, V], strategy) => { + const VComp = V.Component + if (VComp === NotAType) { + return Returns(V, (e, v) => v.map( + f => math.resolve(name, [E, math.typeOf(f)], strategy)(e, f))) + } + const compOp = math.resolve(name, [E, VComp], strategy) + return Returns( + Vector(compOp.returns, (e, v) => v.map(f => compOp(e, f)))) + }), + match([Vector, Vector], (math, [V, W], strategy) => { + const VComp = V.Component + const WComp = W.Component + let compOp + let opNoV + let opNoW + let ReturnType + if (VComp === NotAType || WComp === NotAType) { + const typ = math.typeOf + compOp = (v, w) => { + return math.resolve(name, [typ(v), typ(w)], strategy)(v, w) + } + opNoV = compOp + opNoW = compOp + ReturnType = Vector(NotAType) + } else { + compOp = math.resolve(name, [VComp, WComp], strategy) + opNoV = math.resolve(name, [Undefined, WComp], strategy) + opNoW = math.resolve(name, [VComp, Undefined], strategy) + ReturnType = Vector(compOp.returns) + } + return Returns( + ReturnType, + (v, w) => { + const vInc = Number(v.length > 1) + const wInc = Number(w.length >= v.length || w.length > 1) + const retval = [] + let vIx = 0 + let wIx = 0 + while ((vInc && vIx < v.length) + || (wInc && wIx < w.length) + ) { + if (vIx >= v.length) { + retval.push(opNoV(undefined, w[wIx])) + } else if (wIx >= w.length) { + retval.push(opNoW(v[vIx], undefined)) + } else retval.push(compOp(v[vIx], w[wIx])) + vIx += vInc + wIx += wInc + } + return retval + }) + }) +] diff --git a/src/vector/logical.js b/src/vector/logical.js new file mode 100644 index 0000000..26db8cc --- /dev/null +++ b/src/vector/logical.js @@ -0,0 +1,7 @@ +import {promoteBinary, promoteUnary} from './helpers.js' + +export const not = promoteUnary('not') +export const and = promoteBinary('and') +export const or = promoteBinary('or') + + diff --git a/src/vector/relational.js b/src/vector/relational.js index 23e5835..97e41a0 100644 --- a/src/vector/relational.js +++ b/src/vector/relational.js @@ -1,4 +1,6 @@ import {Vector} from './Vector.js' +import {promoteBinary} from './helpers.js' + import {NotAType, Returns, Undefined} from '#core/Type.js' import {Any, Optional, match} from '#core/TypePatterns.js' import {BooleanT} from '#boolean/BooleanT.js' @@ -23,11 +25,21 @@ export const indistinguishable = [ match([Vector, Any, Optional([Any, Any])], (math, [V, E, T]) => { const VComp = V.Component if (T.length === 0) { // no tolerances + if (VComp === NotAType) { + return Returns(V, (v, e) => v.map(f => { + return math.indistinguishable.resolve([math.typeOf(f), E])(f, e) + })) + } const same = math.indistinguishable.resolve([VComp, E]) return Returns( Vector(same.returns), (v, e) => v.map(f => same(f, e))) } const [[RT, AT]] = T + if (VComp === NotAType) { + return Returns(V, (v, e, [[rT, aT]]) => v.map(f => { + return math.indistinguishable(f, e, rT, aT) + })) + } const same = math.indistinguishable.resolve([VComp, E, RT, AT]) return Returns( Vector(same.returns), @@ -38,11 +50,21 @@ export const indistinguishable = [ // same is symmetric, even though it probably is const VComp = V.Component if (T.length === 0) { // no tolerances + if (VComp === NotAType) { + return Returns(V, (e, v) => v.map(f => { + return math.indistinguishable.resolve([E, math.typeOf(f)])(e, f) + })) + } const same = math.indistinguishable.resolve([E, VComp]) return Returns( Vector(same.returns), (e, v) => v.map(f => same(e, f))) } const [[RT, AT]] = T + if (VComp === NotAType) { + return Returns(V, (e, v, [[rT, aT]]) => v.map(f => { + return math.indistiguishable(e, f, rT, aT) + })) + } const same = math.indistinguishable.resolve([E, VComp, RT, AT]) return Returns( Vector(same.returns), @@ -51,10 +73,15 @@ export const indistinguishable = [ match([Vector, Vector, Optional([Any, Any])], (math, [V, W, T]) => { const VComp = V.Component const WComp = W.Component + const inhomogeneous = VComp === NotAType || WComp === NotAType let same let sameNoV let sameNoW - if (T.length === 0) { // no tolerances + if (inhomogeneous) { + same = math.indistinguishable + sameNoV = same + sameNoW = same + } else if (T.length === 0) { // no tolerances same = math.indistinguishable.resolve([VComp, WComp]) sameNoV = math.indistinguishable.resolve([Undefined, WComp]) sameNoW = math.indistinguishable.resolve([VComp, Undefined]) @@ -65,7 +92,7 @@ export const indistinguishable = [ sameNoW = math.indistinguishable.resolve([VComp, Undefined, RT, AT]) } return Returns( - Vector(same.returns), + inhomogeneous ? Vector(NotAType) : Vector(same.returns), (v, w, [tol = [0, 0]]) => { const [rT, aT] = tol const vInc = Number(v.length > 1) @@ -73,7 +100,6 @@ export const indistinguishable = [ const retval = [] let vIx = 0 let wIx = 0 - let remainder = vIx < v.length || wIx < w.length while ((vInc && vIx < v.length) || (wInc && wIx < w.length) ) { @@ -89,3 +115,6 @@ export const indistinguishable = [ }) }) ] + +export const compare = promoteBinary('compare') +export const exceeds = promoteBinary('exceeds') -- 2.43.0 From ec97b0e20afc532ba931c5373c70ebfae26a9c64 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Fri, 2 May 2025 21:20:45 -0700 Subject: [PATCH 08/12] refactor: rename absquare to normsq and add norm --- src/boolean/__test__/BooleanT.spec.js | 2 +- src/complex/__test__/arithmetic.spec.js | 15 +++++++++++---- src/complex/arithmetic.js | 14 +++++++------- src/generic/arithmetic.js | 11 +++++++---- src/number/__test__/arithmetic.spec.js | 3 ++- src/number/arithmetic.js | 3 ++- src/vector/relational.js | 4 ++-- 7 files changed, 32 insertions(+), 20 deletions(-) diff --git a/src/boolean/__test__/BooleanT.spec.js b/src/boolean/__test__/BooleanT.spec.js index 748a2e8..73ce66d 100644 --- a/src/boolean/__test__/BooleanT.spec.js +++ b/src/boolean/__test__/BooleanT.spec.js @@ -11,7 +11,7 @@ describe('BooleanT Type', () => { }) it('autoconverts to number type', () => { assert.strictEqual(math.abs(false), 0) - assert.strictEqual(math.absquare(true), 1) + assert.strictEqual(math.normsq(true), 1) assert.strictEqual(math.add(true, true), 2) assert.strictEqual(math.divide(false, true), 0) assert.strictEqual(math.cbrt(true), 1) diff --git a/src/complex/__test__/arithmetic.spec.js b/src/complex/__test__/arithmetic.spec.js index 4a54f42..6194370 100644 --- a/src/complex/__test__/arithmetic.spec.js +++ b/src/complex/__test__/arithmetic.spec.js @@ -9,10 +9,17 @@ const CplxNum = Complex(NumberT) const {full} = ReturnTyping describe('complex arithmetic operations', () => { - it('computes absquare of complex numbers', () => { - assert.strictEqual(math.absquare(cplx(3, 4)), 25) - assert.strictEqual(math.absquare(cplx(cplx(2, 3), cplx(4,5))), 54) - assert.strictEqual(math.absquare(cplx(true, true)), 2) + it('computes norm-square of complex numbers', () => { + assert.strictEqual(math.normsq(cplx(3, 4)), 25) + assert.strictEqual(math.normsq(cplx(cplx(2, 3), cplx(4, 5))), 54) + assert.strictEqual(math.normsq(cplx(true, true)), 2) + }) + it('computes norm-magnitude of complex numbers', () => { + assert.strictEqual(math.norm(cplx(-3, 4)), 5) + assert.strictEqual( + math.norm(cplx(cplx(2, -3), cplx(-4, 5))), Math.sqrt(54)) + const boolnorm = math.norm(cplx(true, true)) + assert(math.equal(boolnorm * boolnorm, 2)) }) it('adds complex numbers', () => { const z = cplx(3, 4) diff --git a/src/complex/arithmetic.js b/src/complex/arithmetic.js index f839c5b..0346ddf 100644 --- a/src/complex/arithmetic.js +++ b/src/complex/arithmetic.js @@ -7,11 +7,11 @@ import {ReturnsAs} from '#generic/helpers.js' const {conservative, full, free} = ReturnTyping -export const absquare = match(Complex, (math, C, strategy) => { - const compAbsq = math.absquare.resolve(C.Component, full) - const R = compAbsq.returns +export const normsq = match(Complex, (math, C, strategy) => { + const compNormsq = math.normsq.resolve(C.Component, full) + const R = compNormsq.returns const add = math.add.resolve([R,R], strategy) - return ReturnsAs(add, z => add(compAbsq(z.re), compAbsq(z.im))) + return ReturnsAs(add, z => add(compNormsq(z.re), compNormsq(z.im))) }) export const add = promoteBinary('add') @@ -38,12 +38,12 @@ export const divide = [ export const invert = match(Complex, (math, C, strategy) => { const conj = math.conj.resolve(C, full) - const norm = math.absquare.resolve(C, full) - const div = math.divide.resolve([C.Component, norm.returns], full) + const normsq = math.normsq.resolve(C, full) + const div = math.divide.resolve([C.Component, normsq.returns], full) const cplx = maybeComplex(math, strategy, div.returns, div.returns) return ReturnsAs(cplx, z => { const c = conj(z) - const d = norm(z) + const d = normsq(z) return cplx(div(c.re, d), div(c.im, d)) }) }) diff --git a/src/generic/arithmetic.js b/src/generic/arithmetic.js index d561912..2c170fd 100644 --- a/src/generic/arithmetic.js +++ b/src/generic/arithmetic.js @@ -3,11 +3,14 @@ import {ReturnsAs} from './helpers.js' import {Returns, ReturnTyping} from '#core/Type.js' import {match, Any} from '#core/TypePatterns.js' -export const abs = match(Any, (math, T) => { - const absq = math.absquare.resolve(T) - const sqrt = math.sqrt.resolve(absq.returns, ReturnTyping.conservative) - return ReturnsAs(sqrt, t => sqrt(absq(t))) +export const norm = match(Any, (math, T) => { + const normsq = math.normsq.resolve(T) + const sqrt = math.sqrt.resolve(normsq.returns, ReturnTyping.conservative) + return ReturnsAs(sqrt, t => sqrt(normsq(t))) }) + +export const abs = norm // coincide for most types (scalars) + export const conj = match(Any, (_math, T) => Returns(T, t => t)) export const square = match(Any, (math, T, strategy) => { const mult = math.multiply.resolve([T, T], strategy) diff --git a/src/number/__test__/arithmetic.spec.js b/src/number/__test__/arithmetic.spec.js index 6d718ef..8a2a973 100644 --- a/src/number/__test__/arithmetic.spec.js +++ b/src/number/__test__/arithmetic.spec.js @@ -5,7 +5,8 @@ import {ReturnTyping} from '#core/Type.js' describe('number arithmetic', () => { it('supports basic operations', () => { assert.strictEqual(math.abs(-Infinity), Infinity) - assert.strictEqual(math.absquare(-2), 4) + assert(isNaN(math.norm(NaN))) + assert.strictEqual(math.normsq(-2), 4) assert.strictEqual(math.add(2, 2), 4) assert.strictEqual(math.divide(6, 4), 1.5) assert.strictEqual(math.cbrt(-8), -2) diff --git a/src/number/arithmetic.js b/src/number/arithmetic.js index 0b5c66f..1b2e004 100644 --- a/src/number/arithmetic.js +++ b/src/number/arithmetic.js @@ -7,7 +7,8 @@ import {Complex} from '#complex/Complex.js' const {conservative, full} = ReturnTyping export const abs = plain(Math.abs) -export const absquare = plain(a => a*a) +export const norm = abs +export const normsq = plain(a => a*a) export const add = plain((a, b) => a + b) export const divide = plain((a, b) => a / b) export const cbrt = plain(a => { diff --git a/src/vector/relational.js b/src/vector/relational.js index 97e41a0..18cf0ac 100644 --- a/src/vector/relational.js +++ b/src/vector/relational.js @@ -39,7 +39,7 @@ export const indistinguishable = [ return Returns(V, (v, e, [[rT, aT]]) => v.map(f => { return math.indistinguishable(f, e, rT, aT) })) - } + } const same = math.indistinguishable.resolve([VComp, E, RT, AT]) return Returns( Vector(same.returns), @@ -54,7 +54,7 @@ export const indistinguishable = [ return Returns(V, (e, v) => v.map(f => { return math.indistinguishable.resolve([E, math.typeOf(f)])(e, f) })) - } + } const same = math.indistinguishable.resolve([E, VComp]) return Returns( Vector(same.returns), (e, v) => v.map(f => same(e, f))) -- 2.43.0 From cb3a93dd1c320a18d2fe1ae3fd3040d7583fd1ef Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sat, 3 May 2025 19:59:44 -0700 Subject: [PATCH 09/12] feat: Start adding vector arithmetic So far, abs, add, norm, normsq, and sum are supported. To get them to work, also implements the following: * refactor: Use ReturnType function rather than just accessing .returns * feat: distinguish marking a function as a behavior from its return type * refactor: Rename `NotAType` to `Unknown` because it must be made closer to a bona fide type for the sake of inhomogeneous vectors * feat: make resolving a TypeDispatcher method on a type vector including `Unknown` into a no-op; that simplifies a number of generic behaviors * feat: add `haszero` method parallel to `hasnan` * feat: track the Vector nesting depth of Vector specializations --- src/complex/__test__/type.spec.js | 6 +-- src/complex/arithmetic.js | 40 +++++++++------- src/complex/helpers.js | 10 ++-- src/complex/type.js | 14 +++--- src/core/README.md | 14 ++++-- src/core/Type.js | 7 +-- src/core/TypeDispatcher.js | 21 ++++---- src/core/TypePatterns.js | 8 ++-- src/core/__test__/Type.spec.js | 4 +- src/core/__test__/TypeDispatcher.spec.js | 14 +++--- src/coretypes/utils.js | 30 +++++++----- src/generic/__test__/arithmetic.spec.js | 4 +- src/generic/arithmetic.js | 8 ++-- src/generic/helpers.js | 4 +- src/generic/logical.js | 6 +-- src/generic/relational.js | 16 +++---- src/number/arithmetic.js | 8 +++- src/vector/Vector.js | 10 ++-- src/vector/__test__/Vector.spec.js | 4 +- src/vector/__test__/arithmetic.spec.js | 33 +++++++++++++ src/vector/__test__/type.spec.js | 8 ++-- src/vector/all.js | 1 + src/vector/arithmetic.js | 33 +++++++++++++ src/vector/helpers.js | 61 +++++++++--------------- src/vector/relational.js | 39 +++------------ src/vector/type.js | 6 +-- 26 files changed, 238 insertions(+), 171 deletions(-) create mode 100644 src/vector/__test__/arithmetic.spec.js create mode 100644 src/vector/arithmetic.js diff --git a/src/complex/__test__/type.spec.js b/src/complex/__test__/type.spec.js index 295d0f3..fa19704 100644 --- a/src/complex/__test__/type.spec.js +++ b/src/complex/__test__/type.spec.js @@ -1,7 +1,7 @@ import assert from 'assert' import math from '#nanomath' import {Complex} from '../Complex.js' -import {OneOf, ReturnTyping} from '#core/Type.js' +import {OneOf, ReturnType, ReturnTyping} from '#core/Type.js' import {NumberT} from '#number/NumberT.js' @@ -38,13 +38,13 @@ describe('complex type operations', () => { }) it('computes cis of an angle', () => { const cis = math.cis.resolve(NumberT) - assert.strictEqual(cis.returns, OneOf(Complex(NumberT), NumberT)) + assert.strictEqual(ReturnType(cis), OneOf(Complex(NumberT), NumberT)) assert.strictEqual(cis(0), 1) assert.strictEqual(cis(Math.PI), -1) assert(math.equal(cis(Math.PI/3), cplx(0.5, Math.sqrt(3)/2))) math.config.returnTyping = ReturnTyping.full const ccis = math.cis.resolve(NumberT) - assert.strictEqual(ccis.returns, Complex(NumberT)) + assert.strictEqual(ReturnType(ccis), Complex(NumberT)) const one = ccis(0) assert(one !== 1) assert(math.equal(one, 1)) diff --git a/src/complex/arithmetic.js b/src/complex/arithmetic.js index 0346ddf..045c0f9 100644 --- a/src/complex/arithmetic.js +++ b/src/complex/arithmetic.js @@ -1,7 +1,7 @@ import {Complex} from './Complex.js' import {maybeComplex, promoteBinary, promoteUnary} from './helpers.js' import {ResolutionError} from '#core/helpers.js' -import {Returns, ReturnTyping} from '#core/Type.js' +import {Returns, ReturnType, ReturnTyping} from '#core/Type.js' import {match} from '#core/TypePatterns.js' import {ReturnsAs} from '#generic/helpers.js' @@ -9,7 +9,7 @@ const {conservative, full, free} = ReturnTyping export const normsq = match(Complex, (math, C, strategy) => { const compNormsq = math.normsq.resolve(C.Component, full) - const R = compNormsq.returns + const R = ReturnType(compNormsq) const add = math.add.resolve([R,R], strategy) return ReturnsAs(add, z => add(compNormsq(z.re), compNormsq(z.im))) }) @@ -19,19 +19,21 @@ export const add = promoteBinary('add') export const conj = match(Complex, (math, C, strategy) => { const neg = math.negate.resolve(C.Component, full) const compConj = math.conj.resolve(C.Component, full) - const cplx = maybeComplex(math, strategy, compConj.returns, neg.returns) + const cplx = maybeComplex( + math, strategy, ReturnType(compConj), ReturnType(neg)) return ReturnsAs(cplx, z => cplx(compConj(z.re), neg(z.im))) }) export const divide = [ match([Complex, T => !T.complex], (math, [C, R], strategy) => { const div = math.divide.resolve([C.Component, R], full) - const cplx = maybeComplex(math, strategy, div.returns, div.returns) + const NewComp = ReturnType(div) + const cplx = maybeComplex(math, strategy, NewComp, NewComp) return ReturnsAs(cplx, (z, r) => cplx(div(z.re, r), div(z.im, r))) }), match([Complex, Complex], (math, [W, Z], strategy) => { const inv = math.invert.resolve(Z, full) - const mult = math.multiply.resolve([W, inv.returns], strategy) + const mult = math.multiply.resolve([W, ReturnType(inv)], strategy) return ReturnsAs(mult, (w, z) => mult(w, inv(z))) }) ] @@ -39,8 +41,9 @@ export const divide = [ export const invert = match(Complex, (math, C, strategy) => { const conj = math.conj.resolve(C, full) const normsq = math.normsq.resolve(C, full) - const div = math.divide.resolve([C.Component, normsq.returns], full) - const cplx = maybeComplex(math, strategy, div.returns, div.returns) + const div = math.divide.resolve([C.Component, ReturnType(normsq)], full) + const NewComp = ReturnType(div) + const cplx = maybeComplex(math, strategy, NewComp, NewComp) return ReturnsAs(cplx, z => { const c = conj(z) const d = normsq(z) @@ -53,7 +56,8 @@ export const invert = match(Complex, (math, C, strategy) => { export const multiply = [ match([T => !T.complex, Complex], (math, [R, C], strategy) => { const mult = math.multiply.resolve([R, C.Component], full) - const cplx = maybeComplex(math, strategy, mult.returns, mult.returns) + const NewComp = ReturnType(mult) + const cplx = maybeComplex(math, strategy, NewComp, NewComp) return ReturnsAs(cplx, (r, z) => cplx(mult(r, z.re), mult(r, z.im))) }), match([Complex, T => !T.complex], (math, [C, R], strategy) => { @@ -62,15 +66,19 @@ export const multiply = [ }), match([Complex, Complex], (math, [W, Z], strategy) => { const conj = math.conj.resolve(W.Component, full) - if (conj.returns !== W.Component) { + if (ReturnType(conj) !== W.Component) { throw new ResolutionError( - `conjugation on ${W.Component} returns type (${conj.returns})`) + `conjugation on ${W.Component} returns different type ` + + `(${ReturnType(conj)})`) } const mWZ = math.multiply.resolve([W.Component, Z.Component], full) const mZW = math.multiply.resolve([Z.Component, W.Component], full) - const sub = math.subtract.resolve([mWZ.returns, mZW.returns], full) - const add = math.add.resolve([mWZ.returns, mZW.returns], full) - const cplx = maybeComplex(math, strategy, sub.returns, add.returns) + const TWZ = ReturnType(mWZ) + const TZW = ReturnType(mZW) + const sub = math.subtract.resolve([TWZ, TZW], full) + const add = math.add.resolve([TWZ, TZW], full) + const cplx = maybeComplex( + math, strategy, ReturnType(sub), ReturnType(add)) return ReturnsAs(cplx, (w, z) => { const real = sub(mWZ( w.re, z.re), mZW(z.im, conj(w.im))) const imag = add(mWZ(conj(w.re), z.im), mZW(z.re, w.im)) @@ -85,7 +93,7 @@ export const negate = promoteUnary('negate') // integer coordinates. export const sqrt = match(Complex, (math, C, strategy) => { const re = math.re.resolve(C) - const R = re.returns + const R = ReturnType(re) const isReal = math.isReal.resolve(C) // dependencies for the real case: const zComp = math.zero(C.Component) @@ -98,8 +106,8 @@ export const sqrt = match(Complex, (math, C, strategy) => { const cplx = math.complex.resolve([C.Component, C.Component], full) // additional dependencies for the complex case const abs = math.abs.resolve(C, full) - if (abs.returns !== R) { - throw new TypeError(`abs on ${C} returns ${abs.returns}, not ${R}`) + if (ReturnType(abs) !== R) { + throw new TypeError(`abs on ${C} returns ${ReturnType(abs)}, not ${R}`) } const addRR = math.add.resolve([R, R], conservative) const twoR = addRR(oneR, oneR) diff --git a/src/complex/helpers.js b/src/complex/helpers.js index c6c4aac..1ed8471 100644 --- a/src/complex/helpers.js +++ b/src/complex/helpers.js @@ -1,5 +1,5 @@ import {Complex} from './Complex.js' -import {ReturnTyping} from '#core/Type.js' +import {ReturnType, ReturnTyping} from '#core/Type.js' import {match} from '#core/TypePatterns.js' import {ReturnsAs} from '#generic/helpers.js' @@ -8,7 +8,7 @@ const {free, full} = ReturnTyping export const maybeComplex = (math, strategy, Real, Imag) => { if (strategy !== free) return math.complex.resolve([Real, Imag], strategy) const cplx = math.complex.resolve([Real, Imag], full) - const prune = math.pruneImaginary.resolve(cplx.returns, full) + const prune = math.pruneImaginary.resolve(ReturnType(cplx), full) return ReturnsAs(prune, (r, m) => prune(cplx(r, m))) } @@ -17,7 +17,8 @@ export const promoteUnary = (name, overrideStrategy) => match( (math, C, strategy) => { const compOp = math.resolve(name, C.Component, full) if (overrideStrategy) strategy = overrideStrategy - const cplx = maybeComplex(math, strategy, compOp.returns, compOp.returns) + const NewComp = ReturnType(compOp) + const cplx = maybeComplex(math, strategy, NewComp, NewComp) return ReturnsAs(cplx, z => cplx(compOp(z.re), compOp(z.im))) }) @@ -32,7 +33,8 @@ export const promoteBinary = name => match( [Complex, Complex], (math, [W, Z], strategy) => { const compOp = math.resolve(name, [W.Component, Z.Component], full) - const cplx = maybeComplex(math, strategy, compOp.returns, compOp.returns) + const NewComp = ReturnType(compOp) + const cplx = maybeComplex(math, strategy, NewComp, NewComp) return ReturnsAs( cplx, (w, z) => cplx(compOp(w.re, z.re), compOp(w.im, z.im))) }) diff --git a/src/complex/type.js b/src/complex/type.js index 812ee06..b3586e4 100644 --- a/src/complex/type.js +++ b/src/complex/type.js @@ -1,5 +1,7 @@ import {Complex} from './Complex.js' -import {OneOf, Returns, ReturnTyping, TypeOfTypes} from "#core/Type.js" +import { + OneOf, Returns, ReturnType, ReturnTyping, TypeOfTypes +} from "#core/Type.js" import {Any, match} from "#core/TypePatterns.js" import {BooleanT} from '#boolean/BooleanT.js' import {NumberT} from '#number/NumberT.js' @@ -32,7 +34,7 @@ export const complex = [ export const arg = // [ // enable when we have atan2 in mathjs // match(Complex, (math, C) => { // const re = math.re.resolve(C) -// const R = re.returns +// const R = ReturnType(re) // const im = math.im.resolve(C) // const abs = math.abs.resolve(C) // const atan2 = math.atan2.resolve([R, R], conservative) @@ -54,11 +56,11 @@ export const associate = match( } const eq = math.equal.resolve([W, Z], full) const neg = math.negate.resolve(Z, full) - const eqN = math.equal.resolve([W, neg.returns], full) + const eqN = math.equal.resolve([W, ReturnType(neg)], full) const mult = math.multiply.resolve([Z, Z], full) - const eqM = math.equal.resolve([W, mult.returns], full) - const negM = math.negate.resolve(mult.returns, full) - const eqNM = math.equal.resolve([W, negM.returns], full) + const eqM = math.equal.resolve([W, ReturnType(mult)], full) + const negM = math.negate.resolve(ReturnType(mult), full) + const eqNM = math.equal.resolve([W, ReturnType(negM)], full) const iZ = math.complex(math.zero(Z.Component), math.one(Z.Component)) return Returns(BooleanT, (w, z) => { if (eq(w, z) || eqN(w, neg(z))) return true diff --git a/src/core/README.md b/src/core/README.md index 0cdfd69..77a20d4 100644 --- a/src/core/README.md +++ b/src/core/README.md @@ -13,12 +13,16 @@ As of this writing, the only two types required to be in a TypeDispatcher are Undefined (the type inhabited only by `undefined`) and TypeOfTypes (the type inhabited exactly by Type objects). -There is also a constant NotAType which is the type-world analogue of NaN for -numbers. It is occasionally used for the rare behavior that truly does not +There is also a constant Unknown which is the type that is used when it is +not possible in advance to determine what the type of an entity is or will +be. It is, for example, used for the occasional behavior that truly does not return any particular type, such as the method `zero` that takes a Type and -returns its zero element. However, it does not really work as a Type, and in -particular, do _not_ merge it into any TypeDispatcher -- it will disrupt the -type and method resolution process. +returns its zero element. It is also used for the component type of instances +of containers, like Vector, that have inhomogeneous contents -- and therefore, +as the return type of `sum` on a `Vector(Unknown)`. However, it lacks much of +the machinery of other Type entities. In particular, do _not_ attempt to merge +Unknown as a type into any TypeDispatcher -- it will disrupt the type and +method resolution process. ## Core methods diff --git a/src/core/Type.js b/src/core/Type.js index daa8ca7..7bb7e56 100644 --- a/src/core/Type.js +++ b/src/core/Type.js @@ -66,8 +66,8 @@ export const Undefined = new Type( t => typeof t === 'undefined', {zero: undefined, one: undefined, nan: undefined}) export const TypeOfTypes = new Type(t => t instanceof Type) -export const NotAType = new Type(() => true) // Danger, do not merge! -NotAType._doNotMerge = true +export const Unknown = new Type(() => true) // Danger, do not merge! +Unknown._doNotMerge = true const unionDirectory = new ArrayKeyedMap() // make sure only one of each union export const OneOf = (...types) => { @@ -101,7 +101,8 @@ export const OneOf = (...types) => { return unionDirectory.get(typeList) } -export const Returns = (type, f) => (f.returns = type, f) +export const Returns = (type, f) => (f.returns = type, f.isBehavior = true, f) +export const ReturnType = f => f.returns ?? Unknown export const whichType = typs => Returns(TypeOfTypes, item => { for (const type of Object.values(typs)) { diff --git a/src/core/TypeDispatcher.js b/src/core/TypeDispatcher.js index 941d888..19b2917 100644 --- a/src/core/TypeDispatcher.js +++ b/src/core/TypeDispatcher.js @@ -3,7 +3,9 @@ import ArrayKeyedMap from 'array-keyed-map' import {ResolutionError, isPlainFunction, isPlainObject} from './helpers.js' import {Implementations, ImplementationsGenerator} from './Implementations.js' import {bootstrapTypes} from './type.js' -import {Returns, ReturnTyping, whichType, Type} from './Type.js' +import { + Returns, ReturnType, ReturnTyping, Unknown, Type, whichType +} from './Type.js' import { matched, needsCollection, Passthru, Matcher, match } from './TypePatterns.js' @@ -253,7 +255,7 @@ export class TypeDispatcher { if ('actual' in template) { // incorporate conversion let convert = template.convertor // Check if it's a factory: - if (!convert.returns) convert = convert(this, template.actual) + if (!convert.isBehavior) convert = convert(this, template.actual) extractor = args => convert(args[from]) } return state ? extractor : args => [extractor(args)] @@ -273,6 +275,14 @@ export class TypeDispatcher { throw new ReferenceError(`no method or value for key '${key}'`) } if (!Array.isArray(types)) types = [types] + if (types.some(T => T === Unknown)) { + if (!strategy) return this[key] + const thisTypeOf = whichType(this.types) + return (...args) => { + const types = args.map(thisTypeOf) + return this.resolve(key, types, strategy)(...args) + } + } if (!strategy) { // Avoid recursing on obtaining config if (key === 'config') strategy = ReturnTyping.free @@ -380,12 +390,7 @@ export class TypeDispatcher { finalBehavior = theBehavior if (typeof theBehavior === 'function') { - const returning = theBehavior.returns - if (!returning) { - throw new TypeError( - `No return type specified for ${key} on ${types} with` - + ` return typing ${ReturnTyping.name(strategy)}`) - } + const returning = ReturnType(theBehavior) if (needsCollection(template)) { // have to wrap the behavior to collect the actual arguments // in the way corresponding to the template. Generating that diff --git a/src/core/TypePatterns.js b/src/core/TypePatterns.js index a67a3b9..3fe6d4a 100644 --- a/src/core/TypePatterns.js +++ b/src/core/TypePatterns.js @@ -1,4 +1,4 @@ -import {Type, Undefined} from './Type.js' +import {ReturnType, Type, Undefined, Unknown} from './Type.js' import {isPlainFunction} from './helpers.js' export class TypePattern { @@ -189,8 +189,10 @@ export const matched = (template, math) => { } if (template.matched) { let convert = template.convertor - if (!convert.returns) convert = convert(math, template.actual) - return convert.returns || template.matched + if (!convert.isBehavior) convert = convert(math, template.actual) + const ConvertsTo = ReturnType(convert) + if (ConvertsTo !== Unknown) return ConvertsTo + return template.matched } return template } diff --git a/src/core/__test__/Type.spec.js b/src/core/__test__/Type.spec.js index 4570333..b7eeadf 100644 --- a/src/core/__test__/Type.spec.js +++ b/src/core/__test__/Type.spec.js @@ -2,7 +2,7 @@ import assert from 'assert' import math from '#nanomath' import {NumberT} from '#number/NumberT.js' -import {Returns, ReturnTyping} from '../Type.js' +import {Returns, ReturnType, ReturnTyping} from '../Type.js' import {isPlainFunction} from '../helpers.js' describe('Core types', () => { @@ -36,7 +36,7 @@ describe('Core types', () => { it('provides a return-value labeling', () => { const labeledF = Returns(math.types.Undefined, () => undefined) assert.strictEqual(typeof labeledF, 'function') - assert.strictEqual(labeledF.returns, math.types.Undefined) + assert.strictEqual(ReturnType(labeledF), math.types.Undefined) assert(isPlainFunction(labeledF)) }) diff --git a/src/core/__test__/TypeDispatcher.spec.js b/src/core/__test__/TypeDispatcher.spec.js index e8e1cf6..fc4d035 100644 --- a/src/core/__test__/TypeDispatcher.spec.js +++ b/src/core/__test__/TypeDispatcher.spec.js @@ -6,7 +6,7 @@ import * as numbers from '#number/all.js' import {NumberT} from '#number/NumberT.js' import {ResolutionError} from "#core/helpers.js" import {match, Any} from "#core/TypePatterns.js" -import {NotAType, Returns, ReturnTyping} from "#core/Type.js" +import {Returns, ReturnType, ReturnTyping, Unknown} from "#core/Type.js" import {plain} from "#number/helpers.js" describe('TypeDispatcher', () => { @@ -18,7 +18,7 @@ describe('TypeDispatcher', () => { const {NumberT, TypeOfTypes, Undefined} = incremental.types assert(NumberT.test(7)) assert.strictEqual(incremental.add(-1.5, 0.5), -1) - assert.throws(() => incremental.add(7, undefined), ResolutionError) + assert.throws(() => incremental.add(7, NumberT), ResolutionError) // Make Undefined act like zero: incremental.merge({add: [ match([Undefined, Any], (_m, [_U, T]) => Returns(T, (_a, b) => b)), @@ -26,11 +26,11 @@ describe('TypeDispatcher', () => { ]}) assert.strictEqual(incremental.add(7, undefined), 7) assert.strictEqual( - incremental.resolve('add', [TypeOfTypes, Undefined]).returns, + ReturnType(incremental.resolve('add', [TypeOfTypes, Undefined])), TypeOfTypes) assert.strictEqual(incremental.add(undefined, -3.25), -3.25) assert.strictEqual( - incremental.add.resolve([Undefined, NumberT]).returns, + ReturnType(incremental.add.resolve([Undefined, NumberT])), NumberT) // Oops, changed my mind ;-), make it work like NaN with numbers: const alwaysNaN = Returns(NumberT, () => NaN) @@ -40,7 +40,7 @@ describe('TypeDispatcher', () => { ]}) assert(isNaN(incremental.add(undefined, -3.25))) assert.strictEqual( - incremental.add.resolve([Undefined, NumberT]).returns, + ReturnType(incremental.add.resolve([Undefined, NumberT])), NumberT) assert.strictEqual(incremental.isnan(NaN), 1) incremental.merge(booleans) @@ -68,9 +68,9 @@ describe('TypeDispatcher', () => { assert(!bgn._behaviors.negate.has([ReturnTyping.free, BooleanT])) assert.strictEqual(bgn.negate(true), -2) }) - it('disallows merging NotAType', () => { + it('disallows merging Unknown', () => { const doomed = new TypeDispatcher() - assert.throws(() => doomed.merge({NaT: NotAType}), TypeError) + assert.throws(() => doomed.merge({Unknown}), TypeError) }) it('supports generic types', () => { assert.throws(() => NumberT(0), TypeError) diff --git a/src/coretypes/utils.js b/src/coretypes/utils.js index e89b74c..122c6db 100644 --- a/src/coretypes/utils.js +++ b/src/coretypes/utils.js @@ -1,15 +1,23 @@ -import {NotAType, Returns, TypeOfTypes} from '#core/Type.js' +import {Returns, TypeOfTypes, Unknown} from '#core/Type.js' import {match,Any} from "#core/TypePatterns.js" import {boolnum} from "#number/helpers.js" +export const haszero = [ + match(Any, (math, T) => { + const answer = math.haszero(T) + return Returns(math.typeOf(answer), () => answer) + }), + match(TypeOfTypes, boolnum(T => 'zero' in T)) +] + export const zero = [ match(Any, (math, T) => { const z = math.zero(T) return Returns(T, () => z) }), - match(TypeOfTypes, Returns(NotAType, t => { - if ('zero' in t) return t.zero - throw new RangeError(`type '${t}' has no zero element`) + match(TypeOfTypes, Returns(Unknown, T => { + if ('zero' in T) return T.zero + throw new RangeError(`type '${T}' has no zero element`) })) ] @@ -18,10 +26,10 @@ export const one = [ const unit = math.one(T) return Returns(T, () => unit) }), - match(TypeOfTypes, Returns(NotAType, t => { - if ('one' in t) return t.one + match(TypeOfTypes, Returns(Unknown, T => { + if ('one' in T) return T.one throw new RangeError( - `type '${t}' has no unit element designated as "one"`) + `type '${T}' has no unit element designated as "one"`) })) ] @@ -30,7 +38,7 @@ export const hasnan = [ const answer = math.hasnan(T) return Returns(math.typeOf(answer), () => answer) }), - match(TypeOfTypes, boolnum(t => 'nan' in t)) + match(TypeOfTypes, boolnum(T => 'nan' in T)) ] export const nan = [ @@ -38,9 +46,9 @@ export const nan = [ const notanum = math.nan(T) return Returns(T, () => notanum) }), - match(TypeOfTypes, Returns(NotAType, t => { - if ('nan' in t) return t.nan + match(TypeOfTypes, Returns(Unknown, T => { + if ('nan' in T) return T.nan throw new RangeError( - `type '${t}' has no "not a number" element`) + `type '${T}' has no "not a number" element`) })) ] diff --git a/src/generic/__test__/arithmetic.spec.js b/src/generic/__test__/arithmetic.spec.js index e4a71c0..35d72bd 100644 --- a/src/generic/__test__/arithmetic.spec.js +++ b/src/generic/__test__/arithmetic.spec.js @@ -1,6 +1,6 @@ import assert from 'assert' import math from '#nanomath' -import {ReturnTyping} from '#core/Type.js' +import {ReturnType, ReturnTyping} from '#core/Type.js' const {Complex, NumberT} = math.types @@ -8,7 +8,7 @@ describe('generic arithmetic', () => { it('squares anything', () => { const sq = math.square assert.strictEqual(sq(7), 49) - assert.strictEqual(math.square.resolve([NumberT]).returns, NumberT) + assert.strictEqual(ReturnType(math.square.resolve([NumberT])), NumberT) assert.deepStrictEqual(sq(math.complex(3, 4)), math.complex(-7, 24)) const eyes = math.complex(0, 2) assert.strictEqual(sq(eyes), -4) diff --git a/src/generic/arithmetic.js b/src/generic/arithmetic.js index 2c170fd..92ebf9b 100644 --- a/src/generic/arithmetic.js +++ b/src/generic/arithmetic.js @@ -1,18 +1,20 @@ import {ReturnsAs} from './helpers.js' -import {Returns, ReturnTyping} from '#core/Type.js' +import {Returns, ReturnType, ReturnTyping} from '#core/Type.js' import {match, Any} from '#core/TypePatterns.js' export const norm = match(Any, (math, T) => { const normsq = math.normsq.resolve(T) - const sqrt = math.sqrt.resolve(normsq.returns, ReturnTyping.conservative) + const sqrt = math.sqrt.resolve( + ReturnType(normsq), ReturnTyping.conservative) return ReturnsAs(sqrt, t => sqrt(normsq(t))) }) export const abs = norm // coincide for most types (scalars) export const conj = match(Any, (_math, T) => Returns(T, t => t)) + export const square = match(Any, (math, T, strategy) => { const mult = math.multiply.resolve([T, T], strategy) - return Returns(mult.returns, t => mult(t, t)) + return ReturnsAs(mult, t => mult(t, t)) }) diff --git a/src/generic/helpers.js b/src/generic/helpers.js index 976f78d..a244712 100644 --- a/src/generic/helpers.js +++ b/src/generic/helpers.js @@ -1 +1,3 @@ -export const ReturnsAs = (g, f) => (f.returns = g.returns, f) +import {Returns, ReturnType} from '#core/Type.js' + +export const ReturnsAs = (g, f) => Returns(ReturnType(g), f) diff --git a/src/generic/logical.js b/src/generic/logical.js index df90914..e427619 100644 --- a/src/generic/logical.js +++ b/src/generic/logical.js @@ -1,6 +1,6 @@ import {ReturnsAs} from './helpers.js' -import {OneOf, Returns} from '#core/Type.js' +import {OneOf, Returns, ReturnType} from '#core/Type.js' import {Any, Multiple, match} from '#core/TypePatterns.js' import {boolnum} from '#number/helpers.js' @@ -18,7 +18,7 @@ export const and = [ }), match([Any, Any, Any, Multiple(Any)], (math, [T, U, V, Rest]) => { const andRest = math.and.resolve([U, V, ...Rest]) - const andFirst = math.and.resolve([T, andRest.returns]) + const andFirst = math.and.resolve([T, ReturnType(andRest)]) return ReturnsAs( andFirst, (t, u, v, rest) => andFirst(t, andRest(u, v, ...rest))) }) @@ -33,7 +33,7 @@ export const or = [ }), match([Any, Any, Any, Multiple(Any)], (math, [T, U, V, Rest]) => { const orRest = math.or.resolve([U, V, ...Rest]) - const orFirst = math.and.resolve([T, orRest.returns]) + const orFirst = math.and.resolve([T, ReturnType(orRest)]) return ReturnsAs( orFirst, (t, u, v, rest) => orFirst(t, orRest(u, v, ...rest))) }) diff --git a/src/generic/relational.js b/src/generic/relational.js index 15ce19a..9a5adeb 100644 --- a/src/generic/relational.js +++ b/src/generic/relational.js @@ -1,5 +1,5 @@ import {ReturnsAs} from './helpers.js' -import {Returns, ReturnTyping} from '#core/Type.js' +import {Returns, ReturnType, ReturnTyping} from '#core/Type.js' import {Any, Passthru, match, matched} from '#core/TypePatterns.js' import {boolnum} from '#number/helpers.js' @@ -82,15 +82,15 @@ export const sign = match(Any, (math, T) => { export const unequal = match(Passthru, (math, types) => { const eq = math.equal.resolve(types) - const not = math.not.resolve(eq.returns) + const not = math.not.resolve(ReturnType(eq)) return ReturnsAs(not, (...args) => not(eq(...args))) }) export const larger = match([Any, Any], (math, [T, U]) => { const eq = math.equal.resolve([T, U]) const bigger = math.exceeds.resolve([T, U]) - const not = math.not.resolve(eq.returns) - const and = math.and.resolve([not.returns, bigger.returns]) + const not = math.not.resolve(ReturnType(eq)) + const and = math.and.resolve([ReturnType(not), ReturnType(bigger)]) return ReturnsAs(and, (t, u) => and(not(eq(t, u)), bigger(t, u))) }) @@ -103,21 +103,21 @@ export const isPositive = match(Any, (math, T) => { export const largerEq = match([Any, Any], (math, [T, U]) => { const eq = math.equal.resolve([T, U]) const bigger = math.exceeds.resolve([T, U]) - const or = math.or.resolve([eq.returns, bigger.returns]) + const or = math.or.resolve([ReturnType(eq), ReturnType(bigger)]) return ReturnsAs(or, (t, u) => or(eq(t, u), bigger(t, u))) }) export const smaller = match([Any, Any], (math, [T, U]) => { const eq = math.equal.resolve([T, U]) const bigger = math.exceeds.resolve([U, T]) - const not = math.not.resolve(eq.returns) - const and = math.and.resolve([not.returns, bigger.returns]) + const not = math.not.resolve(ReturnType(eq)) + const and = math.and.resolve([ReturnType(not), ReturnType(bigger)]) return ReturnsAs(and, (t, u) => and(not(eq(t, u)), bigger(u, t))) }) export const smallerEq = match([Any, Any], (math, [T, U]) => { const eq = math.equal.resolve([T, U]) const bigger = math.exceeds.resolve([U, T]) - const or = math.or.resolve([eq.returns, bigger.returns]) + const or = math.or.resolve([ReturnType(eq), ReturnType(bigger)]) return ReturnsAs(or, (t, u) => or(eq(t, u), bigger(u, t))) }) diff --git a/src/number/arithmetic.js b/src/number/arithmetic.js index 1b2e004..71d3d6e 100644 --- a/src/number/arithmetic.js +++ b/src/number/arithmetic.js @@ -1,6 +1,6 @@ import {plain} from './helpers.js' import {NumberT} from './NumberT.js' -import {OneOf, Returns, ReturnTyping} from '#core/Type.js' +import {OneOf, Returns, ReturnTyping, Undefined} from '#core/Type.js' import {match} from '#core/TypePatterns.js' import {Complex} from '#complex/Complex.js' @@ -9,7 +9,11 @@ const {conservative, full} = ReturnTyping export const abs = plain(Math.abs) export const norm = abs export const normsq = plain(a => a*a) -export const add = plain((a, b) => a + b) +export const add = [ + plain((a, b) => a + b), + match([Undefined, NumberT], Returns(NumberT, () => NaN)), + match([NumberT, Undefined], Returns(NumberT, () => NaN)) +] export const divide = plain((a, b) => a / b) export const cbrt = plain(a => { if (a === 0) return a diff --git a/src/vector/Vector.js b/src/vector/Vector.js index 1fbf3f2..573a263 100644 --- a/src/vector/Vector.js +++ b/src/vector/Vector.js @@ -1,4 +1,4 @@ -import {NotAType, Type} from '#core/Type.js' +import {Type, Unknown} from '#core/Type.js' const isVector = v => Array.isArray(v) @@ -12,7 +12,7 @@ export const Vector = new Type(isVector, { if ('zero' in CompType) typeOptions.zero = [CompType.zero] const vectorCompType = new Type(specTest, typeOptions) vectorCompType.Component = CompType - vectorCompType.vector = true + vectorCompType.vectorDepth = (CompType.vectorDepth ?? 0) + 1 return vectorCompType } // Wrapping a generic type in Vector @@ -37,12 +37,12 @@ export const Vector = new Type(isVector, { } }) }, - specializesTo: VT => VT.vector, + specializesTo: VT => 'vectorDepth' in VT && VT.vectorDepth > 0, refine(v, typer) { - if (!v.length) return this.specialize(NotAType) // what else could we do? + if (!v.length) return this.specialize(Unknown) // what else could we do? const eltTypes = v.map(elt => typer(elt)) let compType = eltTypes[0] - if (eltTypes.some(T => T !== compType)) compType = NotAType + if (eltTypes.some(T => T !== compType)) compType = Unknown return this.specialize(compType) } }) diff --git a/src/vector/__test__/Vector.spec.js b/src/vector/__test__/Vector.spec.js index 5233bb5..a934fbf 100644 --- a/src/vector/__test__/Vector.spec.js +++ b/src/vector/__test__/Vector.spec.js @@ -1,6 +1,6 @@ import assert from 'assert' import {Vector} from '../Vector.js' -import {NotAType} from '#core/Type.js' +import {Unknown} from '#core/Type.js' import math from '#nanomath' describe('Vector Type', () => { @@ -14,7 +14,7 @@ describe('Vector Type', () => { const cplx = math.complex assert.strictEqual(typ([3, 4]), Vector(NumberT)) assert.strictEqual(typ([true, false]), Vector(BooleanT)) - assert.strictEqual(typ([3, false]), Vector(NotAType)) + assert.strictEqual(typ([3, false]), Vector(Unknown)) assert.strictEqual(typ([cplx(3, 4), cplx(5)]), Vector(Complex(NumberT))) assert.strictEqual(typ([[3, 4], [5]]), Vector(Vector(NumberT))) }) diff --git a/src/vector/__test__/arithmetic.spec.js b/src/vector/__test__/arithmetic.spec.js new file mode 100644 index 0000000..4ca9c54 --- /dev/null +++ b/src/vector/__test__/arithmetic.spec.js @@ -0,0 +1,33 @@ +import assert from 'assert' +import math from '#nanomath' + +describe('Vector arithmetic functions', () => { + const cplx = math.complex + const cV = [cplx(3, 4), cplx(4, -3), cplx(-3, -4)] + it('distributes abs elementwise', () => { + const abs = math.abs + assert.deepStrictEqual(abs([-3, 4, -5]), [3, 4, 5]) + assert.deepStrictEqual(abs(cV), [5, 5, 5]) + assert.deepStrictEqual(abs([true, -4, cplx(4, 3)]), [1, 4, 5]) + }) + it('computes the norm of a vector or matrix', () => { + const norm = math.norm + assert.strictEqual(norm([-3, 4, -5]), Math.sqrt(50)) + assert.strictEqual(norm([[1, 2], [4, 2]]), 5) + assert.strictEqual(norm(cV), Math.sqrt(75)) + }) + it('adds vectors and matrices', () => { + const add = math.add + assert.deepStrictEqual(add([-3, 4, -5], [8, 0, 2]), [5, 4, -3]) + assert.deepStrictEqual( + add([[1, 2], [4, 2]], [[0, -1], [3, -4]]), [[1, 1], [7, -2]]) + assert.deepStrictEqual( + add([[1, 2], [4, 2]], [0, -1]), [[1, 1], [4, 1]]) + }) + it('computes the sum of a vector', () => { + const sum = math.sum + assert.strictEqual(sum([-3, 4, -5]), -4) + assert.deepStrictEqual(sum(cV), cplx(4, -3)) + assert.strictEqual(sum([4, true, -2]), 3) + }) +}) diff --git a/src/vector/__test__/type.spec.js b/src/vector/__test__/type.spec.js index 43dc101..e50228c 100644 --- a/src/vector/__test__/type.spec.js +++ b/src/vector/__test__/type.spec.js @@ -1,5 +1,5 @@ import assert from 'assert' -import {NotAType} from '#core/Type.js' +import {ReturnType, Unknown} from '#core/Type.js' import math from '#nanomath' describe('Vector type functions', () => { @@ -8,9 +8,11 @@ describe('Vector type functions', () => { const {BooleanT, NumberT, Vector} = math.types assert.deepStrictEqual(vec(3, 4, 5), [3, 4, 5]) assert.strictEqual( - vec.resolve([NumberT, NumberT, NumberT]).returns, Vector(NumberT)) + ReturnType(vec.resolve([NumberT, NumberT, NumberT])), + Vector(NumberT)) assert.deepStrictEqual(vec(3, true), [3, true]) assert.strictEqual( - vec.resolve([NumberT, BooleanT]).returns, Vector(NotAType)) + ReturnType(vec.resolve([NumberT, BooleanT])), + Vector(Unknown)) }) }) diff --git a/src/vector/all.js b/src/vector/all.js index bd58b0b..07af054 100644 --- a/src/vector/all.js +++ b/src/vector/all.js @@ -1,4 +1,5 @@ export * as typeDefinition from './Vector.js' +export * as arithmetic from './arithmetic.js' export * as logical from './logical.js' export * as relational from './relational.js' export * as type from './type.js' diff --git a/src/vector/arithmetic.js b/src/vector/arithmetic.js new file mode 100644 index 0000000..1ca31bb --- /dev/null +++ b/src/vector/arithmetic.js @@ -0,0 +1,33 @@ +import {promoteBinary, promoteUnary} from './helpers.js' +import {Vector} from './Vector.js' + +import {ReturnType} from '#core/Type.js' +import {match} from '#core/TypePatterns.js' +import {ReturnsAs} from '#generic/helpers.js' + +export const normsq = match(Vector, (math, V) => { + const compNormsq = math.normsq.resolve(V.Component) + const sum = math.sum.resolve(Vector(ReturnType(compNormsq))) + return ReturnsAs(sum, v => sum(v.map(compNormsq))) +}) +// abs and norm differ only on Vector (and perhaps other collections) -- +// norm computes overall by the generic formula, whereas abs distributes +// elementwise: +export const abs = promoteUnary('abs') +export const add = promoteBinary('add') + +export const sum = match(Vector, (math, V) => { + const add = math.add.resolve([V.Component, V.Component]) + const haszero = math.haszero(V.Component) + const zero = haszero ? math.zero(V.Component) : undefined + return ReturnsAs(add, v => { + if (v.length === 0) { + if (haszero) return zero + throw new TypeError(`Can't sum empty ${V}: no zero element`) + } + let ix = 0 + let retval = v[ix] + while (++ix < v.length) retval = add(retval, v[ix]) + return retval + }) +}) diff --git a/src/vector/helpers.js b/src/vector/helpers.js index 06ada71..e0ee78f 100644 --- a/src/vector/helpers.js +++ b/src/vector/helpers.js @@ -1,61 +1,44 @@ import {Vector} from './Vector.js' -import {NotAType, Returns, Undefined} from '#core/Type.js' +import {Returns, ReturnType, Undefined} from '#core/Type.js' import {Any, match} from '#core/TypePatterns.js' export const promoteUnary = name => match(Vector, (math, V, strategy) => { - if (V.Component === NotAType) { - // have to resolve element by element :-( - return Returns(V, v => v.map( - elt => math.resolve(name, math.typeOf(elt), strategy)(elt))) - } const compOp = math.resolve(name, V.Component, strategy) - return Returns(Vector(compOp.returns), v => v.map(elt => compOp(elt))) + return Returns(Vector(ReturnType(compOp)), v => v.map(elt => compOp(elt))) }) export const promoteBinary = name => [ match([Vector, Any], (math, [V, E], strategy) => { - const VComp = V.Component - if (VComp === NotAType) { - return Returns(V, (v, e) => v.map( - f => math.resolve(name, [math.typeOf(f), E], strategy)(f, e))) - } - const compOp = math.resolve(name, [VComp, E], strategy) + const compOp = math.resolve(name, [V.Component, E], strategy) return Returns( - Vector(compOp.returns), (v, e) => v.map(f => compOp(f, e))) + Vector(ReturnType(compOp)), (v, e) => v.map(f => compOp(f, e))) }), match([Any, Vector], (math, [E, V], strategy) => { - const VComp = V.Component - if (VComp === NotAType) { - return Returns(V, (e, v) => v.map( - f => math.resolve(name, [E, math.typeOf(f)], strategy)(e, f))) - } - const compOp = math.resolve(name, [E, VComp], strategy) + const compOp = math.resolve(name, [E, V.Component], strategy) return Returns( - Vector(compOp.returns, (e, v) => v.map(f => compOp(e, f)))) + Vector(ReturnType(compOp)), (e, v) => v.map(f => compOp(e, f))) }), match([Vector, Vector], (math, [V, W], strategy) => { const VComp = V.Component const WComp = W.Component - let compOp - let opNoV - let opNoW - let ReturnType - if (VComp === NotAType || WComp === NotAType) { - const typ = math.typeOf - compOp = (v, w) => { - return math.resolve(name, [typ(v), typ(w)], strategy)(v, w) - } - opNoV = compOp - opNoW = compOp - ReturnType = Vector(NotAType) - } else { - compOp = math.resolve(name, [VComp, WComp], strategy) - opNoV = math.resolve(name, [Undefined, WComp], strategy) - opNoW = math.resolve(name, [VComp, Undefined], strategy) - ReturnType = Vector(compOp.returns) + // special case: if the vector nesting depths do not match, + // we operate between the elements of the deeper one and the entire + // more shallow one: + if (V.vectorDepth > W.vectorDepth) { + const compOp = math.resolve(name, [VComp, W], strategy) + return Returns( + Vector(ReturnType(compOp)), (v, w) => v.map(f => compOp(f, w))) } + if (V.vectorDepth < W.vectorDepth) { + const compOp = math.resolve(name, [V, WComp], strategy) + return Returns( + Vector(ReturnType(compOp)), (v, w) => w.map(f => compOp(v, f))) + } + const compOp = math.resolve(name, [VComp, WComp], strategy) + const opNoV = math.resolve(name, [Undefined, WComp], strategy) + const opNoW = math.resolve(name, [VComp, Undefined], strategy) return Returns( - ReturnType, + Vector(ReturnType(compOp)), (v, w) => { const vInc = Number(v.length > 1) const wInc = Number(w.length >= v.length || w.length > 1) diff --git a/src/vector/relational.js b/src/vector/relational.js index 18cf0ac..2dd7d8d 100644 --- a/src/vector/relational.js +++ b/src/vector/relational.js @@ -1,7 +1,7 @@ import {Vector} from './Vector.js' import {promoteBinary} from './helpers.js' -import {NotAType, Returns, Undefined} from '#core/Type.js' +import {Unknown, Returns, ReturnType, Undefined} from '#core/Type.js' import {Any, Optional, match} from '#core/TypePatterns.js' import {BooleanT} from '#boolean/BooleanT.js' @@ -9,11 +9,6 @@ export const deepEqual = [ match([Vector, Any], Returns(BooleanT, () => false)), match([Any, Vector], Returns(BooleanT, () => false)), match([Vector, Vector], (math, [V, W]) => { - if (V.Component === NotAType || W.Component === NotAType) { - return Returns(BooleanT, (v, w) => v === w - || (v.length === w.length - && v.every((e, i) => math.deepEqual(e, w[i])))) - } const compDeep = math.deepEqual.resolve([V.Component, W.Component]) return Returns(BooleanT, (v,w) => v === w || (v.length === w.length @@ -25,24 +20,14 @@ export const indistinguishable = [ match([Vector, Any, Optional([Any, Any])], (math, [V, E, T]) => { const VComp = V.Component if (T.length === 0) { // no tolerances - if (VComp === NotAType) { - return Returns(V, (v, e) => v.map(f => { - return math.indistinguishable.resolve([math.typeOf(f), E])(f, e) - })) - } const same = math.indistinguishable.resolve([VComp, E]) return Returns( - Vector(same.returns), (v, e) => v.map(f => same(f, e))) + Vector(ReturnType(same)), (v, e) => v.map(f => same(f, e))) } const [[RT, AT]] = T - if (VComp === NotAType) { - return Returns(V, (v, e, [[rT, aT]]) => v.map(f => { - return math.indistinguishable(f, e, rT, aT) - })) - } const same = math.indistinguishable.resolve([VComp, E, RT, AT]) return Returns( - Vector(same.returns), + Vector(ReturnType(same)), (v, e, [[rT, aT]]) => v.map(f => same(f, e, rT, aT))) }), match([Any, Vector, Optional([Any, Any])], (math, [E, V, T]) => { @@ -50,30 +35,20 @@ export const indistinguishable = [ // same is symmetric, even though it probably is const VComp = V.Component if (T.length === 0) { // no tolerances - if (VComp === NotAType) { - return Returns(V, (e, v) => v.map(f => { - return math.indistinguishable.resolve([E, math.typeOf(f)])(e, f) - })) - } const same = math.indistinguishable.resolve([E, VComp]) return Returns( - Vector(same.returns), (e, v) => v.map(f => same(e, f))) + Vector(ReturnType(same)), (e, v) => v.map(f => same(e, f))) } const [[RT, AT]] = T - if (VComp === NotAType) { - return Returns(V, (e, v, [[rT, aT]]) => v.map(f => { - return math.indistiguishable(e, f, rT, aT) - })) - } const same = math.indistinguishable.resolve([E, VComp, RT, AT]) return Returns( - Vector(same.returns), + Vector(ReturnType(same)), (e, v, [[rT, aT]]) => v.map(f => same(e, f, rT, aT))) }), match([Vector, Vector, Optional([Any, Any])], (math, [V, W, T]) => { const VComp = V.Component const WComp = W.Component - const inhomogeneous = VComp === NotAType || WComp === NotAType + const inhomogeneous = VComp === Unknown || WComp === Unknown let same let sameNoV let sameNoW @@ -92,7 +67,7 @@ export const indistinguishable = [ sameNoW = math.indistinguishable.resolve([VComp, Undefined, RT, AT]) } return Returns( - inhomogeneous ? Vector(NotAType) : Vector(same.returns), + inhomogeneous ? Vector(Unknown) : Vector(ReturnType(same)), (v, w, [tol = [0, 0]]) => { const [rT, aT] = tol const vInc = Number(v.length > 1) diff --git a/src/vector/type.js b/src/vector/type.js index bad1b64..912c5a7 100644 --- a/src/vector/type.js +++ b/src/vector/type.js @@ -1,11 +1,11 @@ import {Vector} from './Vector.js' -import {NotAType, Returns} from '#core/Type.js' +import {Returns, Unknown} from '#core/Type.js' import {Any, Multiple, match} from '#core/TypePatterns.js' export const vector = match(Multiple(Any), (math, [TV]) => { - if (!TV.length) return Returns(Vector(NotAType), () => []) + if (!TV.length) return Returns(Vector(Unknown), () => []) let CompType = TV[0] - if (TV.some(T => T !== CompType)) CompType = NotAType + if (TV.some(T => T !== CompType)) CompType = Unknown return Returns(Vector(CompType), v => v) }) -- 2.43.0 From f398454d59726749539ad9350f55bd1ec0bde315 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sat, 3 May 2025 20:46:21 -0700 Subject: [PATCH 10/12] feat: vector negate and subtract --- src/number/arithmetic.js | 7 ++++++- src/vector/__test__/arithmetic.spec.js | 6 ++++++ src/vector/arithmetic.js | 3 +++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/number/arithmetic.js b/src/number/arithmetic.js index 71d3d6e..dd03106 100644 --- a/src/number/arithmetic.js +++ b/src/number/arithmetic.js @@ -49,5 +49,10 @@ export const sqrt = match(NumberT, (math, _N, strategy) => { }) }) -export const subtract = plain((a, b) => a - b) +export const subtract = [ + plain((a, b) => a - b), + match([Undefined, NumberT], Returns(NumberT, () => NaN)), + match([NumberT, Undefined], Returns(NumberT, () => NaN)) +] + export const quotient = plain((a,b) => Math.floor(a/b)) diff --git a/src/vector/__test__/arithmetic.spec.js b/src/vector/__test__/arithmetic.spec.js index 4ca9c54..77337b3 100644 --- a/src/vector/__test__/arithmetic.spec.js +++ b/src/vector/__test__/arithmetic.spec.js @@ -24,6 +24,12 @@ describe('Vector arithmetic functions', () => { assert.deepStrictEqual( add([[1, 2], [4, 2]], [0, -1]), [[1, 1], [4, 1]]) }) + it('negates a vector', () => { + assert.deepStrictEqual(math.negate([-3, 4, -5]), [3, -4, 5]) + }) + it('subtracts vectors', () => { + assert.deepStrictEqual(math.subtract([-3, 4, -5], [8, 0, 2]), [-11, 4, -7]) + }) it('computes the sum of a vector', () => { const sum = math.sum assert.strictEqual(sum([-3, 4, -5]), -4) diff --git a/src/vector/arithmetic.js b/src/vector/arithmetic.js index 1ca31bb..4803148 100644 --- a/src/vector/arithmetic.js +++ b/src/vector/arithmetic.js @@ -16,6 +16,9 @@ export const normsq = match(Vector, (math, V) => { export const abs = promoteUnary('abs') export const add = promoteBinary('add') +export const negate = promoteUnary('negate') +export const subtract = promoteBinary('subtract') + export const sum = match(Vector, (math, V) => { const add = math.add.resolve([V.Component, V.Component]) const haszero = math.haszero(V.Component) -- 2.43.0 From ed66ea7772440d146fe7ca7147237f503985faaa Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sun, 4 May 2025 21:12:38 -0700 Subject: [PATCH 11/12] feat: vector transpose and multiply --- src/number/arithmetic.js | 6 ++++- src/vector/__test__/arithmetic.spec.js | 15 +++++++++++ src/vector/__test__/type.spec.js | 7 +++++ src/vector/arithmetic.js | 37 +++++++++++++++++++++++++- src/vector/helpers.js | 18 +++++++++---- src/vector/type.js | 14 +++++++++- 6 files changed, 89 insertions(+), 8 deletions(-) diff --git a/src/number/arithmetic.js b/src/number/arithmetic.js index dd03106..9ad8ac0 100644 --- a/src/number/arithmetic.js +++ b/src/number/arithmetic.js @@ -27,7 +27,11 @@ export const cbrt = plain(a => { return negate ? -result : result }) export const invert = plain(a => 1/a) -export const multiply = plain((a, b) => a * b) +export const multiply = [ + plain((a, b) => a * b), + match([Undefined, NumberT], Returns(NumberT, () => NaN)), + match([NumberT, Undefined], Returns(NumberT, () => NaN)) +] export const negate = plain(a => -a) export const sqrt = match(NumberT, (math, _N, strategy) => { diff --git a/src/vector/__test__/arithmetic.spec.js b/src/vector/__test__/arithmetic.spec.js index 77337b3..5dcc3ca 100644 --- a/src/vector/__test__/arithmetic.spec.js +++ b/src/vector/__test__/arithmetic.spec.js @@ -24,6 +24,21 @@ describe('Vector arithmetic functions', () => { assert.deepStrictEqual( add([[1, 2], [4, 2]], [0, -1]), [[1, 1], [4, 1]]) }) + it('multiplies vectors and matrices', () => { + const mult = math.multiply + const pyth = [3, 4, 5] + assert.deepStrictEqual(mult(pyth, 2), [6, 8, 10]) + assert.deepStrictEqual(mult(-3, pyth), [-9, -12, -15]) + assert.strictEqual(mult(pyth, pyth), 50) + const mat23 = [[1, 2, 3], [-3, -2, -1]] + assert.deepStrictEqual(mult(mat23, pyth), [26, -22]) + const mat32 = math.transpose(mat23) + assert.deepStrictEqual(mult(pyth, mat32), [26, -22]) + assert.deepStrictEqual(mult(mat23, mat32), [[14, -10], [-10, 14]]) + assert.deepStrictEqual( + mult(mat32, [[1, 2], [3, 4]]), + [[-8, -10], [-4, -4], [0, 2]]) + }) it('negates a vector', () => { assert.deepStrictEqual(math.negate([-3, 4, -5]), [3, -4, 5]) }) diff --git a/src/vector/__test__/type.spec.js b/src/vector/__test__/type.spec.js index e50228c..369033a 100644 --- a/src/vector/__test__/type.spec.js +++ b/src/vector/__test__/type.spec.js @@ -15,4 +15,11 @@ describe('Vector type functions', () => { ReturnType(vec.resolve([NumberT, BooleanT])), Vector(Unknown)) }) + it('can transpose vectors and matrices', () => { + const tsp = math.transpose + assert.deepStrictEqual(tsp([3, 4, 5]), [[3], [4], [5]]) + assert.deepStrictEqual(tsp([[1, 2], [3, 4]]), [[1, 3], [2, 4]]) + assert.deepStrictEqual( + tsp([[1, 2, 3], [4, 5, 6]]), [[1, 4], [2, 5], [3, 6]]) + }) }) diff --git a/src/vector/arithmetic.js b/src/vector/arithmetic.js index 4803148..1687138 100644 --- a/src/vector/arithmetic.js +++ b/src/vector/arithmetic.js @@ -1,4 +1,6 @@ -import {promoteBinary, promoteUnary} from './helpers.js' +import { + distributeFirst, distributeSecond, promoteBinary, promoteUnary +} from './helpers.js' import {Vector} from './Vector.js' import {ReturnType} from '#core/Type.js' @@ -16,6 +18,39 @@ export const normsq = match(Vector, (math, V) => { export const abs = promoteUnary('abs') export const add = promoteBinary('add') +export const dotMultiply = promoteBinary('multiply') +export const multiply = [ + distributeFirst('multiply'), + distributeSecond('multiply'), + match([Vector, Vector], (math, [V, W], strategy) => { + const VComp = V.Component + if (W.vectorDepth === 1) { + if (V.vectorDepth === 1) { + const eltWise = math.dotMultiply.resolve([V, W], strategy) + const sum = math.sum.resolve(ReturnType(eltWise)) + return ReturnsAs(sum, (v, w) => sum(eltWise(v, w))) + } + const compMult = math.multiply.resolve([VComp, W], strategy) + return ReturnsAs( + Vector(ReturnType(compMult)), + (v, w) => v.map(f => compMult(f, w))) + } + const transpose = math.transpose.resolve(W, strategy) + const wrapV = V.vectorDepth === 1 + const RowV = wrapV ? V : VComp + const rowMult = math.multiply.resolve([RowV, W.Component], strategy) + let RetType = Vector(ReturnType(rowMult)) + if (!wrapV) RetType = Vector(RetType) + return ReturnsAs(RetType, (v, w) => { + if (wrapV) v = [v] + w = transpose(w) + let retval = v.map(vrow => w.map(wcol => rowMult(vrow, wcol))) + if (wrapV) retval = retval[0] + return retval + }) + }) +] + export const negate = promoteUnary('negate') export const subtract = promoteBinary('subtract') diff --git a/src/vector/helpers.js b/src/vector/helpers.js index e0ee78f..b70bc30 100644 --- a/src/vector/helpers.js +++ b/src/vector/helpers.js @@ -7,17 +7,25 @@ export const promoteUnary = name => match(Vector, (math, V, strategy) => { return Returns(Vector(ReturnType(compOp)), v => v.map(elt => compOp(elt))) }) -export const promoteBinary = name => [ - match([Vector, Any], (math, [V, E], strategy) => { +export const distributeFirst = name => match( + [Vector, Any], + (math, [V, E], strategy) => { const compOp = math.resolve(name, [V.Component, E], strategy) return Returns( Vector(ReturnType(compOp)), (v, e) => v.map(f => compOp(f, e))) - }), - match([Any, Vector], (math, [E, V], strategy) => { + }) + +export const distributeSecond = name => match( + [Any, Vector], + (math, [E, V], strategy) => { const compOp = math.resolve(name, [E, V.Component], strategy) return Returns( Vector(ReturnType(compOp)), (e, v) => v.map(f => compOp(e, f))) - }), + }) + +export const promoteBinary = name => [ + distributeFirst(name), + distributeSecond(name), match([Vector, Vector], (math, [V, W], strategy) => { const VComp = V.Component const WComp = W.Component diff --git a/src/vector/type.js b/src/vector/type.js index 912c5a7..88cbe3f 100644 --- a/src/vector/type.js +++ b/src/vector/type.js @@ -9,4 +9,16 @@ export const vector = match(Multiple(Any), (math, [TV]) => { return Returns(Vector(CompType), v => v) }) - +export const transpose = match(Vector, (_math, V) => { + const wrapV = V.vectorDepth === 1 + const Mat = wrapV ? Vector(V) : V + return Returns(Mat, v => { + if (wrapV) v = [v] + const cols = v.length ? v[0].length : 0 + const retval = [] + for (let ix = 0; ix < cols; ++ix) { + retval.push(v.map(row => row[ix])) + } + return retval + }) +}) -- 2.43.0 From db86b2ecd02ca28f94c3e9569891813bad46bf26 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Tue, 6 May 2025 16:53:11 -0700 Subject: [PATCH 12/12] feat: implement invert for vectors and matrices Here invert returns the pseudoinverse when the input is not invertible. To this end, also implements (rudimentary) conversion of Complex to NumberT, an identity method that produces identity matrices, determinant, and the adjoint operation on matrices (conjugate transpose). --- src/number/type.js | 22 ++- src/vector/__test__/arithmetic.spec.js | 28 ++++ src/vector/__test__/type.spec.js | 20 +++ src/vector/arithmetic.js | 197 ++++++++++++++++++++++++- src/vector/type.js | 125 +++++++++++++++- 5 files changed, 379 insertions(+), 13 deletions(-) diff --git a/src/number/type.js b/src/number/type.js index d8e6806..0d48348 100644 --- a/src/number/type.js +++ b/src/number/type.js @@ -1,8 +1,10 @@ import {plain} from './helpers.js' -import {BooleanT} from '#boolean/BooleanT.js' -import {Returns} from '#core/Type.js' +import {NumberT} from './NumberT.js' + +import {Returns, ReturnType} from '#core/Type.js' import {match} from '#core/TypePatterns.js' -import {NumberT} from '#number/NumberT.js' +import {BooleanT} from '#boolean/BooleanT.js' +import {Complex} from '#complex/Complex.js' const num = f => Returns(NumberT, f) @@ -10,5 +12,17 @@ export const number = [ plain(a => a), // conversions from Boolean should be consistent with one and zero: match(BooleanT, num(p => p ? NumberT.one : NumberT.zero)), - match([], num(() => 0)) + match([], num(() => 0)), + match(Complex, (math, C) => { + const im = math.im.resolve(C) + const re = math.re.resolve(C) + const compNum = math.number.resolve(ReturnType(re)) + const isZ = math.isZero.resolve(ReturnType(im)) + return num(z => { + if (!isZ(im(z))) { + throw new RangeError(`can't convert Complex ${z} to number`) + } + return compNum(re(z)) + }) + }) ] diff --git a/src/vector/__test__/arithmetic.spec.js b/src/vector/__test__/arithmetic.spec.js index 5dcc3ca..ce888d0 100644 --- a/src/vector/__test__/arithmetic.spec.js +++ b/src/vector/__test__/arithmetic.spec.js @@ -24,6 +24,30 @@ describe('Vector arithmetic functions', () => { assert.deepStrictEqual( add([[1, 2], [4, 2]], [0, -1]), [[1, 1], [4, 1]]) }) + it('(pseudo)inverts matrices', () => { + const inv = math.invert + // inverses + assert.deepStrictEqual(inv([3, 4, 5]), [3/50, 2/25, 1/10]) + assert.deepStrictEqual(inv([[4]]), [[0.25]]) + assert.deepStrictEqual(inv([[5, 2], [-7, -3]]), [[3, 2], [-7, -5]]) + assert(math.equal( + inv([[3, 0, 2], [2, 1, 0], [1, 4, 2]]), + [[1/10, 2/5, -1/10], [-1/5, 1/5, 1/5], [7/20, -3/5, 3/20]])) + // pseudoinverses + assert.deepStrictEqual(inv([[1, 0], [1, 0]]), [[1/2, 1/2], [0, 0]]) + assert.deepStrictEqual( + inv([[1, 0], [0, 1], [0, 1]]), [[1, 0, 0], [0, 1/2, 1/2]]) + assert.deepStrictEqual( + inv([[1, 0, 0, 0, 2], + [0, 0, 3, 0, 0], + [0, 0, 0, 0, 0], + [0, 4, 0, 0, 0]]), + [[1/5, 0, 0, 0], + [ 0, 0, 0, 1/4], + [ 0, 1/3, 0, 0], + [ 0, 0, 0, 0], + [2/5, 0, 0, 0]]) + }) it('multiplies vectors and matrices', () => { const mult = math.multiply const pyth = [3, 4, 5] @@ -38,6 +62,10 @@ describe('Vector arithmetic functions', () => { assert.deepStrictEqual( mult(mat32, [[1, 2], [3, 4]]), [[-8, -10], [-4, -4], [0, 2]]) + assert(math.equal( + mult([[3, 0, 2], [2, 1, 0], [1, 4, 2]], + [[1/10, 2/5, -1/10], [-1/5, 1/5, 1/5], [7/20, -3/5, 3/20]]), + [[1, 0, 0], [0, 1, 0], [0, 0, 1]])) }) it('negates a vector', () => { assert.deepStrictEqual(math.negate([-3, 4, -5]), [3, -4, 5]) diff --git a/src/vector/__test__/type.spec.js b/src/vector/__test__/type.spec.js index 369033a..1208ffc 100644 --- a/src/vector/__test__/type.spec.js +++ b/src/vector/__test__/type.spec.js @@ -22,4 +22,24 @@ describe('Vector type functions', () => { assert.deepStrictEqual( tsp([[1, 2, 3], [4, 5, 6]]), [[1, 4], [2, 5], [3, 6]]) }) + it('can take adjoint (conjugate transpose) of a matrix', () => { + const cx = math.complex + assert.deepStrictEqual( + math.adjoint([[cx(1, 1), cx(2, 2)], [cx(3, 3), cx(4, 4)]]), + [[cx(1, -1), cx(3, -3)], [cx(2, -2), cx(4, -4)]]) + }) + it('generates identity from an example matrix or a number of rows', () => { + const id = math.identity + const cx = math.complex + assert.deepStrictEqual(id(2), [[1, 0], [0, 1]]) + assert.deepStrictEqual(id(cx(2)), [[cx(1), cx(0)], [cx(0), cx(1)]]) + assert.deepStrictEqual( + id([[1, 2, 3]]), + [[1, 0 , 0], [0, 1, 0], [0, 0, 1]]) + }) + it('takes the determinant of a matrix', () => { + assert.strictEqual( + math.determinant([[6, 1, 1], [4, -2, 5], [2, 8, 7]]), + -306) + }) }) diff --git a/src/vector/arithmetic.js b/src/vector/arithmetic.js index 1687138..71332c2 100644 --- a/src/vector/arithmetic.js +++ b/src/vector/arithmetic.js @@ -3,7 +3,7 @@ import { } from './helpers.js' import {Vector} from './Vector.js' -import {ReturnType} from '#core/Type.js' +import {Returns, ReturnType} from '#core/Type.js' import {match} from '#core/TypePatterns.js' import {ReturnsAs} from '#generic/helpers.js' @@ -19,6 +19,201 @@ export const abs = promoteUnary('abs') export const add = promoteBinary('add') export const dotMultiply = promoteBinary('multiply') +export const invert = match(Vector, (math, V, strategy) => { + if (V.vectorDepth > 2) { + throw new TypeError( + 'invert not implemented for arrays of dimension > 2') + } + const normsq = math.normsq.resolve(V, strategy) + const NormT = ReturnType(normsq) + const zNorm = math.zero(NormT) + const isRealZ = math.isZero.resolve(NormT, strategy) + if (V.vectorDepth === 1) { + const invNorm = math.invert.resolve(NormT, strategy) + const scalarMult = math.multiply.resolve( + [V, ReturnType(invNorm)], strategy) + return ReturnsAs(scalarMult, v => { + const nsq = normsq(v) + if (isRealZ(nsq)) return Array(v.length).fill(zNorm) + return scalarMult(v, invNorm(nsq)) + }) + } + // usual matrix situation, want to find a matrix whose product with v + // is the identity, or as close as we can get to that if the rank is + // deficient. We use the Moore-Penrose pseudoinverse. + const clone = math.clone.resolve(V, strategy) + const VComp = V.Component + const Elt = VComp.Component + const invElt = math.invert.resolve(Elt, strategy) + const det = math.determinant.resolve(V, strategy) + const neg = math.negate.resolve(Elt, strategy) + const multMM = math.multiply.resolve([V, V], strategy) + const multMS = math.multiply.resolve([V, ReturnType(invElt)], strategy) + const multVS = math.multiply.resolve([VComp, ReturnType(invElt)], strategy) + const multSS = math.multiply.resolve([Elt, Elt], strategy) + const sub = math.subtract.resolve([Elt, Elt], strategy) + const id = math.identity.resolve(V, strategy) + const abs = math.abs.resolve(Elt, strategy) + const gt = math.larger.resolve([NormT, NormT], strategy) + const isEltZ = math.isZero.resolve(Elt, strategy) + const zElt = math.zero(Elt) + const adj = math.adjoint.resolve(V, strategy) + const methods = { + invElt, det, isRealZ, neg, multMM, multMS, multVS, multSS, + id, abs, isEltZ, zElt, gt, sub, adj, clone + } + if (ReturnType(abs) !== NormT) { + throw new TypeError('type inconsistency in matrix invert') + } + return Returns(V, m => { + const rows = m.length + const cols = m[0].length + const nsq = normsq(m) + if (isRealZ(nsq)) { // all-zero matrix + const retval = [] + for (let ix = 0; ix < cols; ++ix) { + retval.push(Array(rows).fill(zNorm)) + } + return retval + } + if (rows == cols) { + // the inv helper will return falsy if not invertible + const retval = inv(m, rows, methods) + if (retval) return retval + } + + return pinv(m, rows, cols, methods) + }) +}) + +// Returns the inverse of a rows×rows matrix or false if not invertible +// Note: destroys m in the inversion process. +function inv( + origm, + rows, + { + invElt, det, isRealZ, neg, multMS, multVS, multSS, + id, abs, isEltZ, zElt, gt, sub, clone + } +) { + switch (rows) { + case 1: return [[invElt(origm[0][0])]] + case 2: { + const dt = det(origm) + if (isRealZ(dt)) return false + const divisor = invElt(dt) + const [[a, b], [c, d]] = origm + return multMS([[d, neg(b)], [neg(c), a]], divisor) + } + default: { // Gauss-Jordan elimination + const m = clone(origm) + const B = id(m) + const cols = rows + // Loop over columns, performing row reductions: + for (let c = 0; c < cols; ++c) { + // Pivot: Find row r that has the largest entry in column c, and + // swap row c and r: + let colMax = abs(m[c][c]) + let rMax = c + for (let r = c + 1; r < rows; ++r) { + const mag = abs(m[r][c]) + if (gt(mag, colMax)) { + colMax = mag + rMax = r + } + } + if (isRealZ(colMax)) return false + if (rMax !== c) { + [m[c], m[rMax], B[c], B[rMax]] + = [m[rMax], m[c], B[rMax], B[c]] + } + // Normalize the cth row: + const normalizer = invElt(m[c][c]) + const mc = multVS(m[c], normalizer) + m[c] = mc + const Bc = multVS(B[c], normalizer) + B[c] = Bc + + // Eliminate nonzero values on other rows at column c + for (let r = 0; r < rows; ++r) { + if (r === c) continue + const mr = m[r] + const Br = B[r] + const mrc = mr[c] + if (!isEltZ(mr[c])) { + // Subtract Arc times row c from row r to eliminate A[r][c] + mr[c] = zElt + for (let s = c + 1; s < cols; ++s) { + mr[s] = sub(mr[s], multSS(mrc, mc[s])) + } + for (let s = 0; s < cols; ++s) { + Br[s] = sub(Br[s], multSS(mrc, Bc[s])) + } + } + } + } + + return B + }} +} + +// Calculates Moore-Penrose pseudoinverse +// uses rank factorization per mathjs; SVD appears to be considered better +// but not worth the effort to implement for this prototype +function pinv(m, rows, cols, methods) { + const {C, F} = rankFactorization(m, rows, cols, methods) + const {multMM, adj} = methods + const Cstar = adj(C) + const Cpinv = multMM(inv(multMM(Cstar, C), Cstar.length, methods), Cstar) + const Fstar = adj(F) + const Fpinv = multMM(Fstar, inv(multMM(F, Fstar), F.length, methods)) + return multMM(Fpinv, Cpinv) +} + +// warning: destroys m in computing the row-reduced echelon form +// TODO: this code should be merged with inv to the extent possible. It's +// a very similar process. +function rref(origm, rows, cols, methods) { + const {isEltZ, invElt, multVS, zElt, sub, multSS, clone} = methods + const m = clone(origm) + let lead = -1 + for (let r = 0; r < rows && ++lead < cols; ++r) { + if (cols <= lead) return m + let i = r + while (isEltZ(m[i][lead])) { + if (++i === rows) { + i = r + if (++lead === cols) return m + } + } + + if (i !== r) [m[i], m[r]] = [m[r], m[i]] + + let normalizer = invElt(m[r][lead]) + const mr = multVS(m[r], normalizer) + m[r] = mr + for (let i = 0; i < rows; ++i) { + if (i === r) continue + const mi = m[i] + const toRemove = mi[lead] + mi[lead] = zElt + for (let j = lead + 1; j < cols; ++j) { + mi[j] = sub(mi[j], multSS(toRemove, mr[j])) + } + } + } + return m +} + +function rankFactorization(m, rows, cols, methods) { + const RREF = rref(m, rows, cols, methods) + const {isEltZ} = methods + const rankRows = RREF.map(row => row.some(elt => !isEltZ(elt))) + const C = m.map(row => row.filter((_, j) => j < rows && rankRows[j])) + const F = RREF.filter((_, i) => rankRows[i]) + return {C, F} +} + export const multiply = [ distributeFirst('multiply'), distributeSecond('multiply'), diff --git a/src/vector/type.js b/src/vector/type.js index 88cbe3f..ad549d5 100644 --- a/src/vector/type.js +++ b/src/vector/type.js @@ -1,5 +1,5 @@ import {Vector} from './Vector.js' -import {Returns, Unknown} from '#core/Type.js' +import {OneOf, Returns, ReturnType, Unknown} from '#core/Type.js' import {Any, Multiple, match} from '#core/TypePatterns.js' export const vector = match(Multiple(Any), (math, [TV]) => { @@ -9,16 +9,125 @@ export const vector = match(Multiple(Any), (math, [TV]) => { return Returns(Vector(CompType), v => v) }) -export const transpose = match(Vector, (_math, V) => { - const wrapV = V.vectorDepth === 1 - const Mat = wrapV ? Vector(V) : V - return Returns(Mat, v => { - if (wrapV) v = [v] +export const determinant = match(Vector(Vector), (math, M, strategy) => { + const Elt = M.Component.Component + const cloneElt = math.clone.resolve(Elt, strategy) + const mult = math.multiply.resolve([Elt, Elt], strategy) + const sub = math.subtract.resolve( + [ReturnType(mult), ReturnType(mult)], strategy) + const isZ = math.isZero.resolve(Elt, strategy) + const zElt = math.zero(Elt) + const clone = math.clone.resolve(M, strategy) + const div = math.divide.resolve([ReturnType(sub), Elt], strategy) + const neg = math.negate.resolve(Elt, strategy) + return Returns(OneOf(ReturnType(clone), ReturnType(sub), Elt), origm => { + const rows = origm.length + switch (rows) { + case 1: return cloneElt(origm[0][0]) + case 2: { + const [[a, b], [c, d]] = origm + return sub(mult(a, d), mult(b, c)) + } + default: { // Bareiss algorithm + const m = clone(origm) + let negated = false + const rowIndices = [...Array(rows).keys()] // track row indices + // because the algorithm may swap rows + for (let k = 0; k < rows; ++k) { + let k_ = rowIndices[k] + if (isZ(m[k_][k])) { + let _k = k + while (++_k < rows) { + if (!isZ(m[rowIndices[_k]][k])) { + k_ = rowIndices[_k] + rowIndices[_k] = rowIndices[k] + rowIndices[k] = k_ + negated = !negated + break + } + } + if (_k === rows) return zElt + } + const piv = m[k_][k] // we now know nonzero + const piv_ = k === 0 ? 1 : m[rowIndices[k-1]][k-1] + for (let i = k + 1; i < rows; ++i) { + const i_ = rowIndices[i] + for (let j = k + 1; j < rows; ++j) { + m[i_][j] = div( + sub(mult(m[i_][j], piv), mult(m[i_][k], m[k_][j])), + piv_) + } + } + } + const det = m[rowIndices[rows - 1]][rows - 1] + return negated ? neg(det) : det + }} + }) +}) + +function identitizer(cols, zero, one) { + const retval = [] + for (let ix = 0; ix < cols; ++ix) { + const row = Array(cols).fill(zero) + row[ix] = one + retval.push(row) + } + return retval +} + +export const identity = [ + match(Any, (math, V) => { + const toNum = math.number.resolve(V) + const zero = math.zero(V) + const one = math.one(V) + return Returns(Vector(Vector(V)), n => identitizer(toNum(n), zero, one)) + }), + match(Vector, (math, V) => { + switch (V.vectorDepth) { + case 1: { + const Elt = V.Component + const one = math.one(Elt) + return Returns(math.typeOf(one), () => one) + } + case 2: { + const Elt = V.Component.Component + const zero = math.zero(Elt) + const one = math.one(Elt) + return Returns(V, m => identitizer( + m.length ? m[0].length : 0, zero, one)) + } + default: + throw new RangeError( + `'identity' not implemented on ${V.vectorDepth} dimensional arrays`) + } + }) +] + +// transposes a 2D matrix +function transposer(wrap, eltFun) { + return v => { + if (wrap) v = [v] const cols = v.length ? v[0].length : 0 const retval = [] for (let ix = 0; ix < cols; ++ix) { - retval.push(v.map(row => row[ix])) + retval.push(v.map(row => eltFun(row[ix]))) } return retval - }) + } +} + +export const transpose = match(Vector, (_math, V) => { + const wrapV = V.vectorDepth === 1 + const Mat = wrapV ? Vector(V) : V + return Returns(Mat, transposer(wrapV, elt => elt)) +}) + +// or with conjugation: +export const adjoint = match(Vector, (math, V, strategy) => { + const wrapV = V.vectorDepth === 1 + const VComp = V.Component + const Elt = wrapV ? VComp : VComp.Component + const conj = math.conj.resolve(Elt, strategy) + const Mat = Vector(Vector(ReturnType(conj))) + return Returns(Mat, transposer(wrapV, conj)) }) -- 2.43.0