From 474cc53d68f0bab2f2a0943c5bbb2607d1cafa15 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Thu, 24 Apr 2025 12:06:47 -0700 Subject: [PATCH 1/6] feat: Start arithmetic functions for complex So far, adds absquare and add. To get these working, especially on mixed types of arguments, this also 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) * Starts tests for complex arithmetic --- src/complex/Complex.js | 8 ++++- src/complex/__test__/arithmetic.spec.js | 26 ++++++++++++++ .../{complex.spec.js => type.spec.js} | 0 src/complex/all.js | 1 + src/complex/arithmetic.js | 17 +++++++++ src/core/TypeDispatcher.js | 2 +- src/core/TypePatterns.js | 36 +++++++++++++++++-- src/generic/relational.js | 2 +- src/nanomath.js | 10 +++++- 9 files changed, 95 insertions(+), 7 deletions(-) create mode 100644 src/complex/__test__/arithmetic.spec.js rename src/complex/__test__/{complex.spec.js => type.spec.js} (100%) create mode 100644 src/complex/arithmetic.js diff --git a/src/complex/Complex.js b/src/complex/Complex.js index d934fd1..541d114 100644 --- a/src/complex/Complex.js +++ b/src/complex/Complex.js @@ -1,4 +1,4 @@ -import {Type} from '#core/Type.js' +import {Returns, Type} from '#core/Type.js' import {match} from '#core/helpers.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..02dd56b --- /dev/null +++ b/src/complex/__test__/arithmetic.spec.js @@ -0,0 +1,26 @@ +import assert from 'assert' +import math from '#nanomath' + +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) + assert.deepStrictEqual(math.add(z, cplx(-1, 1)), cplx(2, 5)) + assert.deepStrictEqual(math.add(z, cplx(true, false)), cplx(4, 4)) + assert.deepStrictEqual(math.add(cplx(false, true), z), cplx(3, 5)) + assert.deepStrictEqual( + math.add(cplx(z, z), cplx(cplx(0.5, 0.5), z)), + cplx(cplx(3.5, 4.5), cplx(6, 8))) + assert.deepStrictEqual(math.add(z, 5), cplx(8, 4)) + assert.deepStrictEqual(math.add(true, z), cplx(4, 4)) + assert.deepStrictEqual(math.add(cplx(z,z), 10), cplx(cplx(13, 4), z)) + assert.deepStrictEqual( + math.add(cplx(z,z), cplx(10,20)), cplx(cplx(13, 4), cplx(23, 4))) + }) +}) diff --git a/src/complex/__test__/complex.spec.js b/src/complex/__test__/type.spec.js similarity index 100% rename from src/complex/__test__/complex.spec.js rename to src/complex/__test__/type.spec.js diff --git a/src/complex/all.js b/src/complex/all.js index 9795516..5c261d4 100644 --- a/src/complex/all.js +++ b/src/complex/all.js @@ -1,2 +1,3 @@ export * as typeDefinition from './Complex.js' +export * as arithmetic from './arithmetic.js' export * as type from './type.js' diff --git a/src/complex/arithmetic.js b/src/complex/arithmetic.js new file mode 100644 index 0000000..0cef720 --- /dev/null +++ b/src/complex/arithmetic.js @@ -0,0 +1,17 @@ +import {Complex} from './Complex.js' +import {match} from '#core/helpers.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 = match([Complex, Complex], (math, [C, D]) => { + const addComps = math.add.resolve([C.Component, D.Component]) + const cplx = math.complex.resolve([addComps.returns, addComps.returns]) + return ReturnsAs( + cplx, (w,z) => cplx(addComps(w.re, z.re), addComps(w.im, z.im))) +}) diff --git a/src/core/TypeDispatcher.js b/src/core/TypeDispatcher.js index 202afe1..121d182 100644 --- a/src/core/TypeDispatcher.js +++ b/src/core/TypeDispatcher.js @@ -334,7 +334,7 @@ export class TypeDispatcher { 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..7c3e8cd 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,34 @@ class SequencePattern extends TypePattern { } } +class PredicatePattern extends TypePattern { + constructor(predicate) { + super() + this.predicate = predicate + } + match(typeSequence, options={}) { + const position = options.position ?? 0 + 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 +173,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 diff --git a/src/generic/relational.js b/src/generic/relational.js index 810c06a..902835d 100644 --- a/src/generic/relational.js +++ b/src/generic/relational.js @@ -18,7 +18,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/nanomath.js b/src/nanomath.js index c133cbf..3038472 100644 --- a/src/nanomath.js +++ b/src/nanomath.js @@ -5,6 +5,14 @@ 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 complex first 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(complex, generics, booleans, coretypes, numbers) export default math -- 2.43.0 From 8da23a84be3eb03de67d6975c204f3032bccb9eb Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Thu, 24 Apr 2025 13:09:15 -0700 Subject: [PATCH 2/6] feat: Add complex argument function `arg` Also removes circular imports from nanomath --- src/boolean/type.js | 2 +- src/complex/Complex.js | 2 +- src/complex/__test__/type.spec.js | 16 ++++++---- src/complex/arithmetic.js | 2 +- src/complex/type.js | 7 +++-- src/core/Implementations.js | 13 ++++++++ src/core/Type.js | 12 -------- src/core/TypeDispatcher.js | 12 ++++---- src/core/TypePatterns.js | 9 ++++++ 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/arithmetic.js | 3 +- src/generic/config.js | 4 +-- src/generic/relational.js | 3 +- src/generic/utils.js | 5 ++-- src/number/NumberT.js | 2 +- src/number/helpers.js | 2 +- src/number/relational.js | 3 +- src/number/type.js | 2 +- src/number/utils.js | 2 +- 24 files changed, 91 insertions(+), 90 deletions(-) create mode 100644 src/core/Implementations.js create mode 100644 src/core/type.js 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 541d114..9dc0630 100644 --- a/src/complex/Complex.js +++ b/src/complex/Complex.js @@ -1,5 +1,5 @@ import {Returns, Type} from '#core/Type.js' -import {match} from '#core/helpers.js' +import {match} from '#core/TypePatterns.js' const isComplex = z => z && typeof z === 'object' && 're' in z && 'im' in z diff --git a/src/complex/__test__/type.spec.js b/src/complex/__test__/type.spec.js index a9d4725..a0c42cf 100644 --- a/src/complex/__test__/type.spec.js +++ b/src/complex/__test__/type.spec.js @@ -1,12 +1,18 @@ import assert from 'assert' import math from '#nanomath' +const cplx = math.complex + 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) + 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) }) }) diff --git a/src/complex/arithmetic.js b/src/complex/arithmetic.js index 0cef720..75a91ea 100644 --- a/src/complex/arithmetic.js +++ b/src/complex/arithmetic.js @@ -1,5 +1,5 @@ import {Complex} from './Complex.js' -import {match} from '#core/helpers.js' +import {match} from '#core/TypePatterns.js' import {ReturnsAs} from '#generic/helpers.js' export const absquare = match(Complex, (math, C) => { diff --git a/src/complex/type.js b/src/complex/type.js index 9881bc5..6c0f410 100644 --- a/src/complex/type.js +++ b/src/complex/type.js @@ -1,7 +1,7 @@ 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 {NumberT} from '#number/NumberT.js' export const complex = [ match(Any, (math, T) => { @@ -25,3 +25,6 @@ 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))) 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..078f12d 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 { @@ -87,13 +85,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 121d182..9eaa6da 100644 --- a/src/core/TypeDispatcher.js +++ b/src/core/TypeDispatcher.js @@ -1,11 +1,12 @@ 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' export class TypeDispatcher { constructor(...specs) { @@ -14,7 +15,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) } diff --git a/src/core/TypePatterns.js b/src/core/TypePatterns.js index 7c3e8cd..a19cb2b 100644 --- a/src/core/TypePatterns.js +++ b/src/core/TypePatterns.js @@ -193,3 +193,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/arithmetic.js b/src/generic/arithmetic.js index fe1556f..7c7c327 100644 --- a/src/generic/arithmetic.js +++ b/src/generic/arithmetic.js @@ -1,6 +1,5 @@ -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 square = match(Any, (math, T) => { const mult = math.multiply.resolve([T, T]) 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 902835d..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]) => { diff --git a/src/generic/utils.js b/src/generic/utils.js index d4c8f87..b3766c4 100644 --- a/src/generic/utils.js +++ b/src/generic/utils.js @@ -1,7 +1,6 @@ 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" export const isZero = match(Passthru, (math, [T]) => { if (!T) { // called with no arguments diff --git a/src/number/NumberT.js b/src/number/NumberT.js index c3816a0..b915b85 100644 --- a/src/number/NumberT.js +++ b/src/number/NumberT.js @@ -1,5 +1,5 @@ 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', { 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)) -- 2.43.0 From 7daa621571e4e5d801fa9ad7add8ee154888e6f3 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Thu, 24 Apr 2025 20:13:35 -0700 Subject: [PATCH 3/6] feat: more Complex methods * Adds associate, conj, multiply, negate, subtract, indistinguishable * As a result equal is now supported * Adds a check for recursive loops in resolve (a key/signature method depending on itself --- src/boolean/BooleanT.js | 1 + src/complex/__test__/arithmetic.spec.js | 53 +++++++++++++++++++++---- src/complex/__test__/type.spec.js | 17 ++++++++ src/complex/all.js | 1 + src/complex/arithmetic.js | 45 ++++++++++++++++----- src/complex/helpers.js | 20 ++++++++++ src/complex/relational.js | 24 +++++++++++ src/complex/type.js | 22 ++++++++++ src/core/TypeDispatcher.js | 8 ++++ src/generic/arithmetic.js | 1 + src/nanomath.js | 10 +++-- src/number/NumberT.js | 1 + 12 files changed, 182 insertions(+), 21 deletions(-) create mode 100644 src/complex/helpers.js create mode 100644 src/complex/relational.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/complex/__test__/arithmetic.spec.js b/src/complex/__test__/arithmetic.spec.js index 02dd56b..d72b335 100644 --- a/src/complex/__test__/arithmetic.spec.js +++ b/src/complex/__test__/arithmetic.spec.js @@ -1,5 +1,7 @@ import assert from 'assert' import math from '#nanomath' +import {Complex} from '../Complex.js' +import {NumberT} from '#number/NumberT.js' const cplx = math.complex @@ -11,16 +13,51 @@ describe('complex arithmetic operations', () => { }) it('adds complex numbers', () => { const z = cplx(3, 4) - assert.deepStrictEqual(math.add(z, cplx(-1, 1)), cplx(2, 5)) - assert.deepStrictEqual(math.add(z, cplx(true, false)), cplx(4, 4)) - assert.deepStrictEqual(math.add(cplx(false, true), z), cplx(3, 5)) + 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( - math.add(cplx(z, z), cplx(cplx(0.5, 0.5), z)), + add(cplx(z, z), cplx(cplx(0.5, 0.5), z)), cplx(cplx(3.5, 4.5), cplx(6, 8))) - assert.deepStrictEqual(math.add(z, 5), cplx(8, 4)) - assert.deepStrictEqual(math.add(true, z), cplx(4, 4)) - assert.deepStrictEqual(math.add(cplx(z,z), 10), cplx(cplx(13, 4), z)) + 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( - math.add(cplx(z,z), cplx(10,20)), cplx(cplx(13, 4), cplx(23, 4))) + 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('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__/type.spec.js b/src/complex/__test__/type.spec.js index a0c42cf..b85797f 100644 --- a/src/complex/__test__/type.spec.js +++ b/src/complex/__test__/type.spec.js @@ -15,4 +15,21 @@ describe('complex type operations', () => { 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)) + }) }) diff --git a/src/complex/all.js b/src/complex/all.js index 5c261d4..8ccff17 100644 --- a/src/complex/all.js +++ b/src/complex/all.js @@ -1,3 +1,4 @@ export * as typeDefinition from './Complex.js' export * as arithmetic from './arithmetic.js' +export * as relational from './relational.js' export * as type from './type.js' diff --git a/src/complex/arithmetic.js b/src/complex/arithmetic.js index 75a91ea..70fafae 100644 --- a/src/complex/arithmetic.js +++ b/src/complex/arithmetic.js @@ -1,17 +1,44 @@ 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))) + 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 = match([Complex, Complex], (math, [C, D]) => { - const addComps = math.add.resolve([C.Component, D.Component]) - const cplx = math.complex.resolve([addComps.returns, addComps.returns]) - return ReturnsAs( - cplx, (w,z) => cplx(addComps(w.re, z.re), addComps(w.im, 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))) }) + +// 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..4d6a8af --- /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 6c0f410..fbe7d8e 100644 --- a/src/complex/type.js +++ b/src/complex/type.js @@ -1,6 +1,7 @@ import {Complex} from './Complex.js' import {Returns} from "#core/Type.js" import {Any, match} from "#core/TypePatterns.js" +import {BooleanT} from '#boolean/BooleanT.js' import {NumberT} from '#number/NumberT.js' export const complex = [ @@ -28,3 +29,24 @@ export const complex = [ 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)) + }) +}) diff --git a/src/core/TypeDispatcher.js b/src/core/TypeDispatcher.js index 9eaa6da..2b997e6 100644 --- a/src/core/TypeDispatcher.js +++ b/src/core/TypeDispatcher.js @@ -8,6 +8,8 @@ import { matched, needsCollection, Passthru, Matcher, match } from './TypePatterns.js' +const underResolution = Symbol('underResolution') + export class TypeDispatcher { constructor(...specs) { this._implementations = {} // maps key to list of [pattern, result] pairs @@ -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,6 +335,7 @@ 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 diff --git a/src/generic/arithmetic.js b/src/generic/arithmetic.js index 7c7c327..cf67fdc 100644 --- a/src/generic/arithmetic.js +++ b/src/generic/arithmetic.js @@ -1,6 +1,7 @@ import {Returns} from '#core/Type.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/nanomath.js b/src/nanomath.js index 3038472..ddeeeba 100644 --- a/src/nanomath.js +++ b/src/nanomath.js @@ -10,9 +10,11 @@ import {TypeDispatcher} from '#core/TypeDispatcher.js' // 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 complex first 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(complex, generics, booleans, coretypes, numbers) +// 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 b915b85..415a37d 100644 --- a/src/number/NumberT.js +++ b/src/number/NumberT.js @@ -4,6 +4,7 @@ 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 -- 2.43.0 From 7903a4611830b252b87b23e367374b6939bd1ec2 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Thu, 24 Apr 2025 20:52:43 -0700 Subject: [PATCH 4/6] feat: add cis and fix indistinguishable --- src/complex/__test__/type.spec.js | 4 ++++ src/complex/relational.js | 4 ++-- src/complex/type.js | 5 ++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/complex/__test__/type.spec.js b/src/complex/__test__/type.spec.js index b85797f..6ea0875 100644 --- a/src/complex/__test__/type.spec.js +++ b/src/complex/__test__/type.spec.js @@ -32,4 +32,8 @@ describe('complex type operations', () => { 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/relational.js b/src/complex/relational.js index 4d6a8af..a8c58f3 100644 --- a/src/complex/relational.js +++ b/src/complex/relational.js @@ -13,11 +13,11 @@ export const indistinguishable = match( return Returns( BooleanT, (w, z) => same(w.re, z.re) && same(w.im, z.im)) } - const [RT, AT] = T + const [[RT, AT]] = T const same = math.indistinguishable.resolve([WComp, ZComp, RT, AT]) return Returns( BooleanT, - (w, z, [rT, aT]) => { + (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 fbe7d8e..e83aabb 100644 --- a/src/complex/type.js +++ b/src/complex/type.js @@ -49,4 +49,7 @@ export const associate = match([Complex, Complex], (math, [W, Z]) => { 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)}))) -- 2.43.0 From 627c1c3d7f3627d36134faee4e31371d776b9f59 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Thu, 24 Apr 2025 21:41:09 -0700 Subject: [PATCH 5/6] feat: add divide and invert and fix PredicatePattern match --- src/complex/__test__/arithmetic.spec.js | 15 +++++++++++++++ src/complex/arithmetic.js | 25 +++++++++++++++++++++++++ src/core/TypePatterns.js | 1 + 3 files changed, 41 insertions(+) diff --git a/src/complex/__test__/arithmetic.spec.js b/src/complex/__test__/arithmetic.spec.js index d72b335..966843b 100644 --- a/src/complex/__test__/arithmetic.spec.js +++ b/src/complex/__test__/arithmetic.spec.js @@ -32,6 +32,21 @@ describe('complex arithmetic operations', () => { 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) diff --git a/src/complex/arithmetic.js b/src/complex/arithmetic.js index 70fafae..d50b993 100644 --- a/src/complex/arithmetic.js +++ b/src/complex/arithmetic.js @@ -20,6 +20,31 @@ export const conj = match(Complex, (math, C) => { 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]) => { diff --git a/src/core/TypePatterns.js b/src/core/TypePatterns.js index a19cb2b..14f41c0 100644 --- a/src/core/TypePatterns.js +++ b/src/core/TypePatterns.js @@ -72,6 +72,7 @@ class PredicatePattern extends TypePattern { } 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] -- 2.43.0 From f9b723b8824a1d6073c95aa2eae399d6e5591998 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Fri, 25 Apr 2025 00:08:29 -0700 Subject: [PATCH 6/6] feat: Add isReal and isZero, and fix specializing a generic type --- src/complex/all.js | 1 + src/complex/utils.js | 9 +++++++++ src/core/Type.js | 5 ++--- src/generic/__test__/utils.spec.js | 12 ++++++++++++ src/generic/utils.js | 5 ++++- 5 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 src/complex/utils.js diff --git a/src/complex/all.js b/src/complex/all.js index 8ccff17..023c144 100644 --- a/src/complex/all.js +++ b/src/complex/all.js @@ -2,3 +2,4 @@ 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/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/Type.js b/src/core/Type.js index 078f12d..555b547 100644 --- a/src/core/Type.js +++ b/src/core/Type.js @@ -10,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 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/utils.js b/src/generic/utils.js index b3766c4..d696d6c 100644 --- a/src/generic/utils.js +++ b/src/generic/utils.js @@ -1,7 +1,10 @@ import {ReturnsAs} from './helpers.js' import {ResolutionError} from '#core/helpers.js' -import {Passthru, match} from "#core/TypePatterns.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') -- 2.43.0