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
This commit is contained in:
Glen Whitney 2025-05-03 19:59:44 -07:00
parent ec97b0e20a
commit cb3a93dd1c
26 changed files with 238 additions and 171 deletions

View file

@ -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))

View file

@ -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)

View file

@ -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)))
})

View file

@ -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

View file

@ -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

View file

@ -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)) {

View file

@ -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

View file

@ -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
}

View file

@ -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))
})

View file

@ -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)

View file

@ -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`)
}))
]

View file

@ -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)

View file

@ -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))
})

View file

@ -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)

View file

@ -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)))
})

View file

@ -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)))
})

View file

@ -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

View file

@ -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)
}
})

View file

@ -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)))
})

View file

@ -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)
})
})

View file

@ -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))
})
})

View file

@ -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'

33
src/vector/arithmetic.js Normal file
View file

@ -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
})
})

View file

@ -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)

View file

@ -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)

View file

@ -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)
})