From aad62df8ac5e3c615febf11a58f1fd1c49e33b60 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Fri, 25 Apr 2025 14:17:34 +0000 Subject: [PATCH] feat: methods on Complex (#24) Adds all of the pocomath functions on Complex that do not depend on any unimplemented types or config properties, except quotient and roundquotient, where the design is murky. To get this working, adds some additional features: * Allows conversions to generic types, with the matched type determined from the return value of the built convertor * Adds predicate-based type patterns * Adds conversion from any non-complex type T to Complex(T) * Adds check for recursive loops in resolve (a key/signature depending on itself) Reviewed-on: https://code.studioinfinity.org/StudioInfinity/nanomath/pulls/24 Co-authored-by: Glen Whitney Co-committed-by: Glen Whitney --- src/boolean/BooleanT.js | 1 + src/boolean/type.js | 2 +- src/complex/Complex.js | 10 ++- src/complex/__test__/arithmetic.spec.js | 78 ++++++++++++++++++++++++ src/complex/__test__/complex.spec.js | 12 ---- src/complex/__test__/type.spec.js | 39 ++++++++++++ src/complex/all.js | 3 + src/complex/arithmetic.js | 69 +++++++++++++++++++++ src/complex/helpers.js | 20 ++++++ src/complex/relational.js | 24 ++++++++ src/complex/type.js | 32 +++++++++- src/complex/utils.js | 9 +++ src/core/Implementations.js | 13 ++++ src/core/Type.js | 17 +----- src/core/TypeDispatcher.js | 22 ++++--- src/core/TypePatterns.js | 46 +++++++++++++- src/core/__test__/TypeDispatcher.spec.js | 4 +- src/core/__test__/helpers.spec.js | 7 +-- src/core/helpers.js | 38 ------------ src/core/type.js | 26 ++++++++ src/coretypes/relational.js | 2 +- src/coretypes/utils.js | 3 +- src/generic/__test__/utils.spec.js | 12 ++++ src/generic/arithmetic.js | 4 +- src/generic/config.js | 4 +- src/generic/relational.js | 5 +- src/generic/utils.js | 8 ++- src/nanomath.js | 12 +++- src/number/NumberT.js | 3 +- src/number/helpers.js | 2 +- src/number/relational.js | 3 +- src/number/type.js | 2 +- src/number/utils.js | 2 +- 33 files changed, 428 insertions(+), 106 deletions(-) create mode 100644 src/complex/__test__/arithmetic.spec.js delete mode 100644 src/complex/__test__/complex.spec.js create mode 100644 src/complex/__test__/type.spec.js create mode 100644 src/complex/arithmetic.js create mode 100644 src/complex/helpers.js create mode 100644 src/complex/relational.js create mode 100644 src/complex/utils.js create mode 100644 src/core/Implementations.js create mode 100644 src/core/type.js diff --git a/src/boolean/BooleanT.js b/src/boolean/BooleanT.js index f2792ed..6f4ede6 100644 --- a/src/boolean/BooleanT.js +++ b/src/boolean/BooleanT.js @@ -1,6 +1,7 @@ import {Type} from '#core/Type.js' export const BooleanT = new Type(n => typeof n === 'boolean', { + typeName: 'BooleanT', zero: false, one: true }) diff --git a/src/boolean/type.js b/src/boolean/type.js index 989bd20..63b2ae7 100644 --- a/src/boolean/type.js +++ b/src/boolean/type.js @@ -1,5 +1,5 @@ import {BooleanT} from './BooleanT.js' -import {match} from '#core/helpers.js' +import {match} from '#core/TypePatterns.js' import {Returns, Type, TypeOfTypes, Undefined} from '#core/Type.js' import {NumberT} from '#number/NumberT.js' diff --git a/src/complex/Complex.js b/src/complex/Complex.js index d934fd1..9dc0630 100644 --- a/src/complex/Complex.js +++ b/src/complex/Complex.js @@ -1,5 +1,5 @@ -import {Type} from '#core/Type.js' -import {match} from '#core/helpers.js' +import {Returns, Type} from '#core/Type.js' +import {match} from '#core/TypePatterns.js' const isComplex = z => z && typeof z === 'object' && 're' in z && 'im' in z @@ -66,6 +66,12 @@ function complexSpecialize(ComponentType) { export const Complex = new Type(isComplex, { specialize: complexSpecialize, specializesTo, + from: [match( // can promote any non-complex type T to Complex(T) as needed + // but watch out, this should be tried late, because it can preclude + // other more reasonable conversions like bool => number. + T => !T.complex, + (math, T) => Returns(Complex(T), r => math.complex(r, math.zero(T))) + )], refine: function(z, typer) { const reType = typer(z.re) const imType = typer(z.im) diff --git a/src/complex/__test__/arithmetic.spec.js b/src/complex/__test__/arithmetic.spec.js new file mode 100644 index 0000000..966843b --- /dev/null +++ b/src/complex/__test__/arithmetic.spec.js @@ -0,0 +1,78 @@ +import assert from 'assert' +import math from '#nanomath' +import {Complex} from '../Complex.js' +import {NumberT} from '#number/NumberT.js' + +const cplx = math.complex + +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('adds complex numbers', () => { + const z = cplx(3, 4) + const add = math.add + assert.deepStrictEqual(add(z, cplx(-1, 1)), cplx(2, 5)) + assert.deepStrictEqual(add(z, cplx(true, false)), cplx(4, 4)) + assert.deepStrictEqual(add(cplx(false, true), z), cplx(3, 5)) + assert.deepStrictEqual( + add(cplx(z, z), cplx(cplx(0.5, 0.5), z)), + cplx(cplx(3.5, 4.5), cplx(6, 8))) + assert.deepStrictEqual(add(z, 5), cplx(8, 4)) + assert.deepStrictEqual(add(true, z), cplx(4, 4)) + assert.deepStrictEqual(add(cplx(z,z), 10), cplx(cplx(13, 4), z)) + assert.deepStrictEqual( + add(cplx(z,z), cplx(10,20)), cplx(cplx(13, 4), cplx(23, 4))) + }) + it('conjugates complex numbers', () => { + const conj = math.conj + const z = cplx(3, 4) + assert.deepStrictEqual(conj(z), cplx(3, -4)) + assert.deepStrictEqual(conj(cplx(z,z)), cplx(cplx(3, -4), cplx(-3, -4))) + }) + it('divides complex numbers', () => { + const div = math.divide + const z = cplx(3, 4) + assert.deepStrictEqual(div(z, cplx(0, 1)), cplx(4, -3)) + assert(math.equal(div(z, z), 1)) + // should probably have a quaternion example, but it's intricate + }) + it('inverts complex numbers', () => { + const inv = math.invert + assert.deepStrictEqual(inv(cplx(0, 1)), cplx(0, -1)) + assert.deepStrictEqual(inv(cplx(3, 4)), cplx(3/25, -4/25)) + assert.deepStrictEqual( + inv(cplx(cplx(1, 2), cplx(4, 2))), + cplx(cplx(1/25, -2/25), cplx(-4/25, -2/25))) + }) + it('multiplies complex numbers', () => { + const mult = math.multiply + const z = cplx(3, 4) + assert.deepStrictEqual(mult(z, z), cplx(-7, 24)) + assert(math.equal(mult(z, math.conj(z)), 25)) + const q0 = cplx(cplx(1, 1), math.zero(Complex(NumberT))) + const q1 = cplx(cplx(1, 0.5), cplx(0.5, 0.75)) + assert.deepStrictEqual( + mult(q0, q1), cplx(cplx(0.5, 1.5), cplx(1.25, 0.25))) + assert.deepStrictEqual( + mult(q0, cplx(cplx(2, 0.1), cplx(1, 0.1))), + cplx(cplx(1.9, 2.1), cplx(1.1, -0.9))) + }) + it('subtracts complex numbers', () => { + const z = cplx(3, 4) + const sub = math.subtract + assert.deepStrictEqual(sub(z, cplx(-1, 1)), cplx(4, 3)) + assert.deepStrictEqual(sub(z, cplx(true, false)), cplx(2, 4)) + assert.deepStrictEqual(sub(cplx(false, true), z), cplx(-3, -3)) + assert.deepStrictEqual( + sub(cplx(z, z), cplx(cplx(0.5, 0.5), z)), + cplx(cplx(2.5, 3.5), cplx(0, 0))) + assert.deepStrictEqual(sub(z, 5), cplx(-2, 4)) + assert.deepStrictEqual(sub(true, z), cplx(-2, -4)) + assert.deepStrictEqual(sub(cplx(z,z), 10), cplx(cplx(-7, 4), z)) + assert.deepStrictEqual( + sub(cplx(z,z), cplx(10,20)), cplx(cplx(-7, 4), cplx(-17, 4))) + }) +}) diff --git a/src/complex/__test__/complex.spec.js b/src/complex/__test__/complex.spec.js deleted file mode 100644 index a9d4725..0000000 --- a/src/complex/__test__/complex.spec.js +++ /dev/null @@ -1,12 +0,0 @@ -import assert from 'assert' -import math from '#nanomath' - -describe('complex type operations', () => { - it('converts to number', () => { - assert.deepStrictEqual(math.complex(3), {re: 3, im: 0}) - assert.deepStrictEqual(math.complex(NaN), {re: NaN, im: NaN}) - assert.deepStrictEqual(math.complex(3, -1), {re: 3, im: -1}) - assert.deepStrictEqual(math.complex(false, true), {re: false, im: true}) - assert.throws(() => math.complex(3, false), RangeError) - }) -}) diff --git a/src/complex/__test__/type.spec.js b/src/complex/__test__/type.spec.js new file mode 100644 index 0000000..6ea0875 --- /dev/null +++ b/src/complex/__test__/type.spec.js @@ -0,0 +1,39 @@ +import assert from 'assert' +import math from '#nanomath' + +const cplx = math.complex + +describe('complex type operations', () => { + it('converts to number', () => { + assert.deepStrictEqual(cplx(3), {re: 3, im: 0}) + assert.deepStrictEqual(cplx(NaN), {re: NaN, im: NaN}) + assert.deepStrictEqual(cplx(3, -1), {re: 3, im: -1}) + assert.deepStrictEqual(cplx(false, true), {re: false, im: true}) + assert.throws(() => cplx(3, false), RangeError) + }) + it('calculates the argument of a complex number', () => { + assert.strictEqual(math.arg(cplx(1, Math.sqrt(3))), Math.PI/3) + assert.strictEqual(math.arg(cplx(true, true)), Math.PI/4) + }) + it('detects associates of a complex number', () => { + const z = cplx(3, 4) + const assoc = math.associate + assert(assoc(z, z)) + assert(assoc(z, cplx(-3, -4))) + assert(assoc(z, cplx(-4, 3))) + assert(assoc(z, cplx(4, -3))) + assert(!assoc(z, math.conj(z))) + const b = cplx(true, true) + assert(assoc(b, cplx(-1, 1))) + assert(assoc(cplx(1, 1), b)) + assert(assoc(cplx(-1, -1), b)) + assert(assoc(cplx(1, -1), b)) + assert(assoc(cplx(-1, 1), b)) + assert(!assoc(b, cplx(false, true))) + assert(!assoc(cplx(0, 1), b)) + }) + it('computes cis of an angle', () => { + assert(math.equal(math.cis(0), 1)) + assert(math.equal(math.cis(Math.PI/3), cplx(0.5, Math.sqrt(3)/2))) + }) +}) diff --git a/src/complex/all.js b/src/complex/all.js index 9795516..023c144 100644 --- a/src/complex/all.js +++ b/src/complex/all.js @@ -1,2 +1,5 @@ export * as typeDefinition from './Complex.js' +export * as arithmetic from './arithmetic.js' +export * as relational from './relational.js' export * as type from './type.js' +export * as utilities from './utils.js' diff --git a/src/complex/arithmetic.js b/src/complex/arithmetic.js new file mode 100644 index 0000000..d50b993 --- /dev/null +++ b/src/complex/arithmetic.js @@ -0,0 +1,69 @@ +import {Complex} from './Complex.js' +import {promoteBinary, promoteUnary} from './helpers.js' +import {ResolutionError} from '#core/helpers.js' +import {match} from '#core/TypePatterns.js' +import {ReturnsAs} from '#generic/helpers.js' + +export const absquare = match(Complex, (math, C) => { + const compAbsq = math.absquare.resolve([C.Component]) + const R = compAbsq.returns + const add = math.add.resolve([R,R]) + return ReturnsAs(add, z => add(compAbsq(z.re), compAbsq(z.im))) +}) + +export const add = promoteBinary('add') + +export const conj = match(Complex, (math, C) => { + const neg = math.negate.resolve(C.Component) + const compConj = math.conj.resolve(C.Component) + const cplx = math.complex.resolve([compConj.returns, neg.returns]) + return ReturnsAs(cplx, z => cplx(compConj(z.re), neg(z.im))) +}) + +export const divide = [ + match([Complex, T => !T.complex], (math, [C, R]) => { + const div = math.divide.resolve([C.Component, R]) + const cplx = math.complex.resolve([div.returns, div.returns]) + return ReturnsAs(cplx, (z, r) => cplx(div(z.re, r), div(z.im, r))) + }), + match([Complex, Complex], (math, [W, Z]) => { + const inv = math.invert.resolve(Z) + const mult = math.multiply.resolve([W, inv.returns]) + return ReturnsAs(mult, (w, z) => mult(w, inv(z))) + }) +] + +export const invert = match(Complex, (math, C) => { + const conj = math.conj.resolve(C) + const norm = math.absquare.resolve(C) + const div = math.divide.resolve([C.Component, norm.returns]) + const cplx = math.complex.resolve([div.returns, div.returns]) + return ReturnsAs(cplx, z => { + const c = conj(z) + const d = norm(z) + return cplx(div(c.re, d), div(c.im, d)) + }) +}) + +// We want this to work for complex numbers, quaternions, octonions, etc +// See https://math.ucr.edu/home/baez/octonions/node5.html +export const multiply = match([Complex, Complex], (math, [W, Z]) => { + const conj = math.conj.resolve(W.Component) + if (conj.returns !== W.Component) { + throw new ResolutionError( + `conjugation on ${W.Component} returns other type (${conj.returns})`) + } + const mWZ = math.multiply.resolve([W.Component, Z.Component]) + const mZW = math.multiply.resolve([Z.Component, W.Component]) + const sub = math.subtract.resolve([mWZ.returns, mZW.returns]) + const add = math.add.resolve([mWZ.returns, mZW.returns]) + const cplx = math.complex.resolve([sub.returns, add.returns]) + 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)) + return cplx(real, imag) + }) +}) + +export const negate = promoteUnary('negate') +export const subtract = promoteBinary('subtract') diff --git a/src/complex/helpers.js b/src/complex/helpers.js new file mode 100644 index 0000000..84e828f --- /dev/null +++ b/src/complex/helpers.js @@ -0,0 +1,20 @@ +import {Complex} from './Complex.js' +import {match} from '#core/TypePatterns.js' +import {ReturnsAs} from '#generic/helpers.js' + +export const promoteUnary = name => match( + Complex, + (math, C) => { + const compOp = math.resolve(name, C.Component) + const cplx = math.complex.resolve([compOp.returns, compOp.returns]) + return ReturnsAs(cplx, z => cplx(compOp(z.re), compOp(z.im))) + }) + +export const promoteBinary = name => match( + [Complex, Complex], + (math, [W, Z]) => { + const compOp = math.resolve(name, [W.Component, Z.Component]) + const cplx = math.complex.resolve([compOp.returns, compOp.returns]) + return ReturnsAs( + cplx, (w, z) => cplx(compOp(w.re, z.re), compOp(w.im, z.im))) + }) diff --git a/src/complex/relational.js b/src/complex/relational.js new file mode 100644 index 0000000..a8c58f3 --- /dev/null +++ b/src/complex/relational.js @@ -0,0 +1,24 @@ +import {Complex} from './Complex.js' +import {BooleanT} from '#boolean/BooleanT.js' +import {Returns} from '#core/Type.js' +import {match, Any, Optional} from '#core/TypePatterns.js' + +export const indistinguishable = match( + [Complex, Complex, Optional([Any, Any])], + (math, [W, Z, T]) => { + let WComp = W.Component + let ZComp = Z.Component + if (T.length === 0) { // no tolerances + const same = math.indistinguishable.resolve([WComp, ZComp]) + return Returns( + BooleanT, (w, z) => same(w.re, z.re) && same(w.im, z.im)) + } + const [[RT, AT]] = T + const same = math.indistinguishable.resolve([WComp, ZComp, RT, AT]) + return Returns( + BooleanT, + (w, z, [[rT, aT]]) => { + return same(w.re, z.re, rT, aT) && same(w.im, z.im, rT, aT) + }) + }) + diff --git a/src/complex/type.js b/src/complex/type.js index 9881bc5..e83aabb 100644 --- a/src/complex/type.js +++ b/src/complex/type.js @@ -1,7 +1,8 @@ import {Complex} from './Complex.js' -import {match} from "#core/helpers.js" import {Returns} from "#core/Type.js" -import {Any} from "#core/TypePatterns.js" +import {Any, match} from "#core/TypePatterns.js" +import {BooleanT} from '#boolean/BooleanT.js' +import {NumberT} from '#number/NumberT.js' export const complex = [ match(Any, (math, T) => { @@ -25,3 +26,30 @@ export const complex = [ return Returns(Complex(T), (r, m) => ({re: r, im: m})) }) ] + +export const arg = match( + Complex(NumberT), Returns(NumberT, z => Math.atan2(z.im, z.re))) + +/* Returns true if w is z multiplied by a complex unit */ +export const associate = match([Complex, Complex], (math, [W, Z]) => { + if (Z.Component.complex) { + throw new Error( + `The group of units of type ${Z} is not yet implemented`) + } + const eq = math.equal.resolve([W, Z]) + const neg = math.negate.resolve(Z) + const eqN = math.equal.resolve([W, neg.returns]) + const mult = math.multiply.resolve([Z, Z]) + const eqM = math.equal.resolve([W, mult.returns]) + const negM = math.negate.resolve(mult.returns) + const eqNM = math.equal.resolve([W, negM.returns]) + 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 + const iz = mult(iZ, z) + return eqM(w, iz) || eqNM(w, negM(iz)) + }) +}) + +export const cis = match(NumberT, Returns(Complex(NumberT), t => ({ + re: Math.cos(t), im: Math.sin(t)}))) diff --git a/src/complex/utils.js b/src/complex/utils.js new file mode 100644 index 0000000..2fef03a --- /dev/null +++ b/src/complex/utils.js @@ -0,0 +1,9 @@ +import {Complex} from './Complex.js' +import {match} from '#core/TypePatterns.js' +import {ReturnsAs} from '#generic/helpers.js' + +export const isReal = match(Complex, (math, C) => { + const eq = math.equal.resolve([C.Component, C.Component]) + const add = math.add.resolve([C.Component, C.Component]) + return ReturnsAs(eq, z => eq(z.re, add(z.re, z.im))) +}) diff --git a/src/core/Implementations.js b/src/core/Implementations.js new file mode 100644 index 0000000..c53ea3e --- /dev/null +++ b/src/core/Implementations.js @@ -0,0 +1,13 @@ +export class Implementations { + constructor(impOrImps) { + if (Array.isArray(impOrImps)) { + this.matchers = impOrImps + } else this.matchers = [impOrImps] + } +} + +export class ImplementationsGenerator { + constructor(f) { + this.generate = f + } +} diff --git a/src/core/Type.js b/src/core/Type.js index 088b9e7..555b547 100644 --- a/src/core/Type.js +++ b/src/core/Type.js @@ -1,6 +1,4 @@ import ArrayKeyedMap from 'array-keyed-map' -import {match} from './helpers.js' -import {Passthru} from './TypePatterns.js' // Generic types are callable, so we have no choice but to extend Function export class Type extends Function { @@ -12,9 +10,8 @@ export class Type extends Function { // let the proxy out of this function, never `this`. const rewired = new Proxy(this, { apply: (target, thisForCall, args) => { - const callThrough = thisForCall ?? target - if (callThrough.specialize) return callThrough.specialize(...args) - throw new TypeError(`Type ${callThrough} is not generic`) + if (target.specialize) return target.specialize(...args) + throw new TypeError(`Type ${target} is not generic`) }, get: (target, prop, receiver) => { if (prop === 'isAproxy') return true @@ -87,13 +84,3 @@ export const whichType = typs => Returns(TypeOfTypes, item => { } throw new TypeError(errorMsg) }) - -export const typeOf = match(Passthru, math => whichType(math.types)) - -// bootstrapping order matters, but order of exports in a module isn't -// simply the order that the items are listed in the module. So we make -// an explicitly ordered export of implementations for this sake: - -export const bootstrapTypes = { - Type, Undefined, TypeOfTypes, typeOf -} diff --git a/src/core/TypeDispatcher.js b/src/core/TypeDispatcher.js index 202afe1..2b997e6 100644 --- a/src/core/TypeDispatcher.js +++ b/src/core/TypeDispatcher.js @@ -1,11 +1,14 @@ 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, whichType, Type} from './Type.js' import { - Implementations, ImplementationsGenerator, Matcher, ResolutionError, - isPlainFunction, isPlainObject, match, types -} from './helpers.js' -import {bootstrapTypes, Returns, whichType, Type} from './Type.js' -import {matched, needsCollection, Passthru} from './TypePatterns.js' + matched, needsCollection, Passthru, Matcher, match +} from './TypePatterns.js' + +const underResolution = Symbol('underResolution') export class TypeDispatcher { constructor(...specs) { @@ -14,7 +17,6 @@ export class TypeDispatcher { this._behaviors = {} // maps key to a map from type vectors to results this._fallbacks = {} // maps key to a catchall result // bootstrap the instance - this.merge({types}) this.merge(bootstrapTypes) for (const spec of specs) this.merge(spec) } @@ -261,6 +263,7 @@ export class TypeDispatcher { if (!(key in this)) { throw new ReferenceError(`no method or value for key '${key}'`) } + if (!Array.isArray(types)) types = [types] const generatingDeps = this.resolve._genDepsOf?.length if (generatingDeps) this._addToDeps(key, types) @@ -269,6 +272,10 @@ export class TypeDispatcher { // Return the cached resolution if it's there if (behave.has(types)) { const result = behave.get(types) + if (result === underResolution) { + throw new ResolutionError( + `recursive resolution of ${key} on ${types}`) + } if (generatingDeps && typeof result === 'object' && !(result instanceof Type) @@ -328,13 +335,14 @@ export class TypeDispatcher { let theBehavior = () => undefined let finalBehavior this.resolve._genDepsOf.push([key, types]) + behave.set(types, underResolution) try { // Used to make sure not to return without popping _genDepsOf if (!('returns' in item)) { // looks like a factory try { theBehavior = item( DependencyRecorder(this, '', this, []), - matched(template)) + matched(template, this)) } catch (e) { e.message = `Error in factory for ${key} on ${types} ` + `(match data ${template}): ${e.message}` diff --git a/src/core/TypePatterns.js b/src/core/TypePatterns.js index eb73afd..14f41c0 100644 --- a/src/core/TypePatterns.js +++ b/src/core/TypePatterns.js @@ -1,4 +1,5 @@ import {Type, Undefined} from './Type.js' +import {isPlainFunction} from './helpers.js' export class TypePattern { match(typeSequence, options={}) { @@ -64,12 +65,35 @@ class SequencePattern extends TypePattern { } } +class PredicatePattern extends TypePattern { + constructor(predicate) { + super() + this.predicate = predicate + } + match(typeSequence, options={}) { + const position = options.position ?? 0 + if (position >= typeSequence.length) return [-1, undefined] + const actual = typeSequence[position] + if (this.predicate(actual)) return [position + 1, actual] + return [-1, Undefined] + } + sampleTypes() { + throw new Error('sampleTypes() not yet implemented for PredicatePattern') + } + equal(other) { + return super.equal(other) && this.predicate === other.predicate + } +} + export const pattern = patternOrSpec => { if (patternOrSpec instanceof TypePattern) return patternOrSpec if (patternOrSpec instanceof Type) { return new MatchTypePattern(patternOrSpec) } if (Array.isArray(patternOrSpec)) return new SequencePattern(patternOrSpec) + if (isPlainFunction(patternOrSpec)) { + return new PredicatePattern(patternOrSpec) + } throw new TypeError(`Can't interpret '${patternOrSpec}' as a type pattern`) } @@ -150,9 +174,16 @@ class PassthruPattern extends TypePattern { export const Passthru = new PassthruPattern() // returns the template just of matched types, dropping any actual types -export const matched = (template) => { - if (Array.isArray(template)) return template.map(matched) - return template.matched ?? template +export const matched = (template, math) => { + if (Array.isArray(template)) { + return template.map(pattern => matched(pattern, math)) + } + if (template.matched) { + let convert = template.convertor + if (!convert.returns) convert = convert(math, template.actual) + return convert.returns || template.matched + } + return template } // checks if the template is just pass-through or needs collection @@ -163,3 +194,12 @@ export const needsCollection = (template) => { } return 'actual' in template } + +export class Matcher { + constructor(spec, facOrBehave) { + this.pattern = pattern(spec) + this.does = facOrBehave + } +} + +export const match = (spec, facOrBehave) => new Matcher(spec, facOrBehave) diff --git a/src/core/__test__/TypeDispatcher.spec.js b/src/core/__test__/TypeDispatcher.spec.js index 37e5618..c843e59 100644 --- a/src/core/__test__/TypeDispatcher.spec.js +++ b/src/core/__test__/TypeDispatcher.spec.js @@ -4,8 +4,8 @@ import * as booleans from '#boolean/all.js' import * as generics from '#generic/all.js' import * as numbers from '#number/all.js' import {NumberT} from '#number/NumberT.js' -import {match, ResolutionError} from "#core/helpers.js" -import {Any} from "#core/TypePatterns.js" +import {ResolutionError} from "#core/helpers.js" +import {match, Any} from "#core/TypePatterns.js" import {Returns, NotAType} from "#core/Type.js" import {plain} from "#number/helpers.js" diff --git a/src/core/__test__/helpers.spec.js b/src/core/__test__/helpers.spec.js index 10849d6..46576eb 100644 --- a/src/core/__test__/helpers.spec.js +++ b/src/core/__test__/helpers.spec.js @@ -1,9 +1,8 @@ import assert from 'assert' -import { - Matcher, match, isPlainObject, isPlainFunction, Implementations -} from '../helpers.js' +import {isPlainObject, isPlainFunction} from '../helpers.js' +import {Implementations} from '../Implementations.js' import {Type, Undefined, TypeOfTypes} from '../Type.js' -import {TypePattern} from '../TypePatterns.js' +import {match, Matcher, TypePattern} from '../TypePatterns.js' describe('Core helpers', () => { it('defines what Matchers are', () => { diff --git a/src/core/helpers.js b/src/core/helpers.js index dc05182..8d7b2b2 100644 --- a/src/core/helpers.js +++ b/src/core/helpers.js @@ -1,41 +1,3 @@ -import {pattern, Passthru} from './TypePatterns.js' - -export class Matcher { - constructor(spec, facOrBehave) { - this.pattern = pattern(spec) - this.does = facOrBehave - } -} - -export class Implementations { - constructor(impOrImps) { - if (Array.isArray(impOrImps)) { - this.matchers = impOrImps - } else this.matchers = [impOrImps] - } -} - -export const match = (spec, facOrBehave) => new Matcher(spec, facOrBehave) - -export class ImplementationsGenerator { - constructor(f) { - this.generate = f - } -} - -// the archetypal example of needing an ImplementationsGenerator: -// each TypeDispatcher must have a types property, which will be a -// plain object of types. This must be a different object for each -// TypeDispatcher, but the same object regardless of the types vector -// passed to resolve. So an ordinary factory won't work, because it -// would make a new plain object for each different types vector that -// the property `types` was resolved with. And just a plain object -// wouldn't work, because then every TypeDispatcher would have the same -// collection of types (and modifying the types in one would affect them -// all). Hence we do: - -export const types = new ImplementationsGenerator(() => match(Passthru, {})) - export class ResolutionError extends TypeError { constructor(...args) { super(...args) diff --git a/src/core/type.js b/src/core/type.js new file mode 100644 index 0000000..7452dd4 --- /dev/null +++ b/src/core/type.js @@ -0,0 +1,26 @@ +import {ImplementationsGenerator} from './Implementations.js' +import {Type, TypeOfTypes, Undefined, whichType} from './Type.js' +import {match, Passthru} from './TypePatterns.js' + +export const typeOf = match(Passthru, math => whichType(math.types)) + +// And now the types object itself: This property provides the archetypal +// example of needing an ImplementationsGenerator. Each TypeDispatcher must +// have a types property, which will be a plain object of types. This object +// must be different for each TypeDispatcher, but within a TypeDispatcher, +// it must be the same object regardless of the types vector passed to resolve. +// So an ordinary factory won't work, because it would make a new plain object +// for each different types vector that the property `types` was resolved with. +// And just a plain object wouldn't work, because then every TypeDispatcher +// would have the same collection of types (and modifying the types in one +// would affect them all). Hence we do: + +export const types = new ImplementationsGenerator(() => match(Passthru, {})) + +// bootstrapping order matters, but order of exports in a module isn't +// simply the order that the items are listed in the module. So we make +// an explicitly ordered export of implementations for this sake: + +export const bootstrapTypes = { + types, Type, Undefined, TypeOfTypes, typeOf +} diff --git a/src/coretypes/relational.js b/src/coretypes/relational.js index 4192dc1..a45cadf 100644 --- a/src/coretypes/relational.js +++ b/src/coretypes/relational.js @@ -1,5 +1,5 @@ -import {match} from '#core/helpers.js' import {TypeOfTypes, Undefined} from '#core/Type.js' +import {match} from '#core/TypePatterns.js' import {boolnum} from '#number/helpers.js' export const indistinguishable = [ diff --git a/src/coretypes/utils.js b/src/coretypes/utils.js index 4386ac1..e89b74c 100644 --- a/src/coretypes/utils.js +++ b/src/coretypes/utils.js @@ -1,6 +1,5 @@ -import {match} from '#core/helpers.js' import {NotAType, Returns, TypeOfTypes} from '#core/Type.js' -import {Any} from "#core/TypePatterns.js" +import {match,Any} from "#core/TypePatterns.js" import {boolnum} from "#number/helpers.js" export const zero = [ diff --git a/src/generic/__test__/utils.spec.js b/src/generic/__test__/utils.spec.js index e178a2d..f8b6d53 100644 --- a/src/generic/__test__/utils.spec.js +++ b/src/generic/__test__/utils.spec.js @@ -1,5 +1,6 @@ import assert from 'assert' import math from '#nanomath' +import {NumberT} from '#number/NumberT.js' describe('generic utility functions', () => { it('tests whether an element is zero', () => { @@ -9,5 +10,16 @@ describe('generic utility functions', () => { assert(isZero(false)) assert(!isZero(true)) assert(isZero(undefined)) + assert(isZero(math.types.Complex(NumberT).zero)) + assert(isZero(math.complex(-2e-16, 4e-17))) + assert(!isZero(math.complex(true))) + }) + it('tests whether an element is real', () => { + const {isReal} = math + assert(isReal(Infinity)) + assert(isReal(false)) + assert(isReal(math.types.Complex(NumberT).one)) + assert(isReal(math.complex(-3.25, 4e-16))) + assert(!isReal(math.complex(3, 4))) }) }) diff --git a/src/generic/arithmetic.js b/src/generic/arithmetic.js index fe1556f..cf67fdc 100644 --- a/src/generic/arithmetic.js +++ b/src/generic/arithmetic.js @@ -1,7 +1,7 @@ -import {match} from '#core/helpers.js' import {Returns} from '#core/Type.js' -import {Any} from '#core/TypePatterns.js' +import {match, Any} from '#core/TypePatterns.js' +export const conj = match(Any, (_math, T) => Returns(T, a => a)) export const square = match(Any, (math, T) => { const mult = math.multiply.resolve([T, T]) return Returns(mult.returns, a => mult(a, a)) diff --git a/src/generic/config.js b/src/generic/config.js index 9f421ec..72f8ced 100644 --- a/src/generic/config.js +++ b/src/generic/config.js @@ -1,5 +1,5 @@ -import {ImplementationsGenerator, match} from '#core/helpers.js' -import {Passthru} from '#core/TypePatterns.js' +import {ImplementationsGenerator} from '#core/Implementations.js' +import {match, Passthru} from '#core/TypePatterns.js' export const config = new ImplementationsGenerator( () => match(Passthru, {relTol: 1e-12, absTol: 1e-15})) diff --git a/src/generic/relational.js b/src/generic/relational.js index 810c06a..bd23f92 100644 --- a/src/generic/relational.js +++ b/src/generic/relational.js @@ -1,7 +1,6 @@ import {ReturnsAs} from './helpers.js' -import {match} from '#core/helpers.js' import {Returns} from '#core/Type.js' -import {Any, Passthru, matched} from '#core/TypePatterns.js' +import {Any, Passthru, match, matched} from '#core/TypePatterns.js' import {boolnum} from '#number/helpers.js' export const equal = match([Any, Any], (math, [T, U]) => { @@ -18,7 +17,7 @@ export const equal = match([Any, Any], (math, [T, U]) => { return boolnum(() => false)(math) } // Get the type of the first argument to the matching checker: - const ByType = matched(exactChecker.template).flat()[0] + const ByType = matched(exactChecker.template, math).flat()[0] // Now see if there are tolerances for that type: const typeConfig = math.resolve('config', [ByType]) if ('relTol' in typeConfig) { diff --git a/src/generic/utils.js b/src/generic/utils.js index d4c8f87..d696d6c 100644 --- a/src/generic/utils.js +++ b/src/generic/utils.js @@ -1,8 +1,10 @@ import {ReturnsAs} from './helpers.js' -import {ResolutionError, match} from '#core/helpers.js' -import {Returns} from '#core/Type.js' -import {Passthru} from "#core/TypePatterns.js" +import {ResolutionError} from '#core/helpers.js' +import {Passthru, match} from '#core/TypePatterns.js' +import {boolnum} from '#number/helpers.js' +// Most types are real. Have to make sure to redefine on all non-real types +export const isReal = match(Passthru, boolnum(() => true)) export const isZero = match(Passthru, (math, [T]) => { if (!T) { // called with no arguments throw new ResolutionError('isZero() requires one argument') diff --git a/src/nanomath.js b/src/nanomath.js index c133cbf..ddeeeba 100644 --- a/src/nanomath.js +++ b/src/nanomath.js @@ -5,6 +5,16 @@ import * as numbers from './number/all.js' import * as complex from './complex/all.js' import {TypeDispatcher} from '#core/TypeDispatcher.js' -const math = new TypeDispatcher(booleans, coretypes, generics, numbers, complex) +// At the moment, since we are not sorting patterns in any way, +// order matters in the construction. Patterns that come later in +// the following list will be tried earlier. (The rationale for that +// ordering is that any time you merge something, it should supersede +// 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_ +// non-complex type to complex, potentially making a poor overload choice) +// to be tried last. +const math = new TypeDispatcher(generics, complex, booleans, coretypes, numbers) export default math diff --git a/src/number/NumberT.js b/src/number/NumberT.js index c3816a0..415a37d 100644 --- a/src/number/NumberT.js +++ b/src/number/NumberT.js @@ -1,9 +1,10 @@ import {Type} from '#core/Type.js' -import {match} from '#core/helpers.js' +import {match} from '#core/TypePatterns.js' import {BooleanT} from '#boolean/BooleanT.js' export const NumberT = new Type(n => typeof n === 'number', { from: match(BooleanT, math => math.number.resolve([BooleanT])), + typeName: 'NumberT', // since used before ever put in a TypeDispatcher one: 1, zero: 0, nan: NaN diff --git a/src/number/helpers.js b/src/number/helpers.js index 4ab75af..1d6bf9b 100644 --- a/src/number/helpers.js +++ b/src/number/helpers.js @@ -1,7 +1,7 @@ import {NumberT} from './NumberT.js' -import {match} from '#core/helpers.js' import {Returns} from '#core/Type.js' +import {match} from '#core/TypePatterns.js' export const plain = f => match( Array(f.length).fill(NumberT), Returns(NumberT, f)) diff --git a/src/number/relational.js b/src/number/relational.js index 8839c37..a4820a6 100644 --- a/src/number/relational.js +++ b/src/number/relational.js @@ -1,6 +1,5 @@ -import {match} from '#core/helpers.js' import {Returns} from '#core/Type.js' -import {Optional} from '#core/TypePatterns.js' +import {match, Optional} from '#core/TypePatterns.js' import {boolnum} from './helpers.js' import {NumberT} from './NumberT.js' diff --git a/src/number/type.js b/src/number/type.js index ddb47cc..d8e6806 100644 --- a/src/number/type.js +++ b/src/number/type.js @@ -1,7 +1,7 @@ import {plain} from './helpers.js' import {BooleanT} from '#boolean/BooleanT.js' -import {match} from '#core/helpers.js' import {Returns} from '#core/Type.js' +import {match} from '#core/TypePatterns.js' import {NumberT} from '#number/NumberT.js' const num = f => Returns(NumberT, f) diff --git a/src/number/utils.js b/src/number/utils.js index 08372c4..01fdbe4 100644 --- a/src/number/utils.js +++ b/src/number/utils.js @@ -2,7 +2,7 @@ import {plain, boolnum} from './helpers.js' import {NumberT} from './NumberT.js' import {Returns} from '#core/Type.js' -import {match} from '#core/helpers.js' +import {match} from '#core/TypePatterns.js' export const clone = plain(a => a) export const isnan = match(NumberT, boolnum(isNaN))