From cb3a93dd1c320a18d2fe1ae3fd3040d7583fd1ef Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sat, 3 May 2025 19:59:44 -0700 Subject: [PATCH] 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) })