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/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/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/__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 f839c5b..045c0f9 100644 --- a/src/complex/arithmetic.js +++ b/src/complex/arithmetic.js @@ -1,17 +1,17 @@ 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' 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 = ReturnType(compNormsq) 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') @@ -19,31 +19,34 @@ 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))) }) ] 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 cplx = maybeComplex(math, strategy, div.returns, div.returns) + const normsq = math.normsq.resolve(C, full) + 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 = norm(z) + const d = normsq(z) return cplx(div(c.re, d), div(c.im, d)) }) }) @@ -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 398aaaa..7bb7e56 100644 --- a/src/core/Type.js +++ b/src/core/Type.js @@ -66,31 +66,43 @@ 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) => { + 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) } -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 c56e39d..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 @@ -309,9 +319,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 +337,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) @@ -373,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 0705894..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 { @@ -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 { @@ -51,7 +53,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] } @@ -63,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 { @@ -83,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 => { @@ -105,6 +110,7 @@ class AnyPattern extends TypePattern { : [-1, Undefined] } sampleTypes() {return [Undefined]} + toString() {return 'Any'} } export const Any = new AnyPattern() @@ -125,12 +131,13 @@ class OptionalPattern extends TypePattern { options.position = newPos matches.push(newMatch) } - return [options.position, matches] + return [options.position, [matches]] } sampleTypes() {return []} equal(other) { return super.equal(other) && this.pattern.equal(other.pattern) } + toString() {return `?${this.pattern}`} } export const Optional = item => new OptionalPattern(item) @@ -148,7 +155,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) } @@ -157,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) @@ -169,6 +177,7 @@ class PassthruPattern extends TypePattern { return [typeSequence.length, typeSequence.slice(position)] } sampleTypes() {return []} + toString() {return 'Passthru'} } export const Passthru = new PassthruPattern() @@ -180,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/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/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/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/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/__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/arithmetic.js b/src/generic/arithmetic.js index d561912..92ebf9b 100644 --- a/src/generic/arithmetic.js +++ b/src/generic/arithmetic.js @@ -1,15 +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 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( + 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 new file mode 100644 index 0000000..e427619 --- /dev/null +++ b/src/generic/logical.js @@ -0,0 +1,40 @@ +import {ReturnsAs} from './helpers.js' + +import {OneOf, Returns, ReturnType} 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, ReturnType(andRest)]) + 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, 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 2d70a1e..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' @@ -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]) @@ -79,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(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]) - return boolnum((t, u) => !eq(t, u) && bigger(t, u))(math) + 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))) }) 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([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]) - return boolnum((t, u) => !eq(t, u) && bigger(u, t))(math) + 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]) - return ReturnsAs(bigger, (t, u) => eq(t, u) || bigger(u, t)) + 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/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/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/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/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/arithmetic.js b/src/number/arithmetic.js index 0b5c66f..9ad8ac0 100644 --- a/src/number/arithmetic.js +++ b/src/number/arithmetic.js @@ -1,14 +1,19 @@ 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' const {conservative, full} = ReturnTyping export const abs = plain(Math.abs) -export const absquare = plain(a => a*a) -export const add = plain((a, b) => a + b) +export const norm = abs +export const normsq = plain(a => a*a) +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 @@ -22,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) => { @@ -44,5 +53,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/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/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/Vector.js b/src/vector/Vector.js new file mode 100644 index 0000000..573a263 --- /dev/null +++ b/src/vector/Vector.js @@ -0,0 +1,48 @@ +import {Type, Unknown} 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.vectorDepth = (CompType.vectorDepth ?? 0) + 1 + 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) => { + 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)) { + throw new TypeError( + `can't refine ${typeName} to ${v}; ` + + `not all entries have type ${newCompType}`) + } + return this.specialize(newCompType) + } + }) + }, + specializesTo: VT => 'vectorDepth' in VT && VT.vectorDepth > 0, + refine(v, typer) { + 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 = Unknown + 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..a934fbf --- /dev/null +++ b/src/vector/__test__/Vector.spec.js @@ -0,0 +1,35 @@ +import assert from 'assert' +import {Vector} from '../Vector.js' +import {Unknown} 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(Unknown)) + 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/__test__/arithmetic.spec.js b/src/vector/__test__/arithmetic.spec.js new file mode 100644 index 0000000..ce888d0 --- /dev/null +++ b/src/vector/__test__/arithmetic.spec.js @@ -0,0 +1,82 @@ +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('(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] + 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]]) + 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]) + }) + 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) + assert.deepStrictEqual(sum(cV), cplx(4, -3)) + assert.strictEqual(sum([4, true, -2]), 3) + }) +}) diff --git a/src/vector/__test__/relational.spec.js b/src/vector/__test__/relational.spec.js new file mode 100644 index 0000000..a6e8871 --- /dev/null +++ b/src/vector/__test__/relational.spec.js @@ -0,0 +1,47 @@ +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]), 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]]) + }) + 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/__test__/type.spec.js b/src/vector/__test__/type.spec.js new file mode 100644 index 0000000..1208ffc --- /dev/null +++ b/src/vector/__test__/type.spec.js @@ -0,0 +1,45 @@ +import assert from 'assert' +import {ReturnType, Unknown} 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( + ReturnType(vec.resolve([NumberT, NumberT, NumberT])), + Vector(NumberT)) + assert.deepStrictEqual(vec(3, true), [3, true]) + assert.strictEqual( + 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]]) + }) + 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/__test__/utils.spec.js b/src/vector/__test__/utils.spec.js new file mode 100644 index 0000000..eb1f0cc --- /dev/null +++ b/src/vector/__test__/utils.spec.js @@ -0,0 +1,41 @@ +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]) + }) + 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/all.js b/src/vector/all.js new file mode 100644 index 0000000..07af054 --- /dev/null +++ b/src/vector/all.js @@ -0,0 +1,7 @@ +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' +export * as utilities from './utils.js' + diff --git a/src/vector/arithmetic.js b/src/vector/arithmetic.js new file mode 100644 index 0000000..71332c2 --- /dev/null +++ b/src/vector/arithmetic.js @@ -0,0 +1,266 @@ +import { + distributeFirst, distributeSecond, promoteBinary, promoteUnary +} from './helpers.js' +import {Vector} from './Vector.js' + +import {Returns, 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 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'), + 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') + +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 new file mode 100644 index 0000000..b70bc30 --- /dev/null +++ b/src/vector/helpers.js @@ -0,0 +1,70 @@ +import {Vector} from './Vector.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) => { + const compOp = math.resolve(name, V.Component, strategy) + return Returns(Vector(ReturnType(compOp)), v => v.map(elt => compOp(elt))) +}) + +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))) + }) + +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 + // 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( + Vector(ReturnType(compOp)), + (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 new file mode 100644 index 0000000..2dd7d8d --- /dev/null +++ b/src/vector/relational.js @@ -0,0 +1,95 @@ +import {Vector} from './Vector.js' +import {promoteBinary} from './helpers.js' + +import {Unknown, Returns, ReturnType, Undefined} from '#core/Type.js' +import {Any, Optional, 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]) => { + 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])))) + }) +] + +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(ReturnType(same)), (v, e) => v.map(f => same(f, e))) + } + const [[RT, AT]] = T + const same = math.indistinguishable.resolve([VComp, E, RT, AT]) + return 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]) => { + // 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(ReturnType(same)), (e, v) => v.map(f => same(e, f))) + } + const [[RT, AT]] = T + const same = math.indistinguishable.resolve([E, VComp, RT, AT]) + return 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 === Unknown || WComp === Unknown + let same + let sameNoV + let sameNoW + 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]) + } 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( + inhomogeneous ? Vector(Unknown) : Vector(ReturnType(same)), + (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 + 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 + }) + }) +] + +export const compare = promoteBinary('compare') +export const exceeds = promoteBinary('exceeds') diff --git a/src/vector/type.js b/src/vector/type.js new file mode 100644 index 0000000..ad549d5 --- /dev/null +++ b/src/vector/type.js @@ -0,0 +1,133 @@ +import {Vector} from './Vector.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]) => { + if (!TV.length) return Returns(Vector(Unknown), () => []) + let CompType = TV[0] + if (TV.some(T => T !== CompType)) CompType = Unknown + return Returns(Vector(CompType), 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 => 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)) +}) diff --git a/src/vector/utils.js b/src/vector/utils.js new file mode 100644 index 0000000..54c6fa9 --- /dev/null +++ b/src/vector/utils.js @@ -0,0 +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')