From 722a2724a0d814e453eaffccb34463a70e6d4a1c Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sat, 26 Apr 2025 18:40:09 -0700 Subject: [PATCH] feat: Make `cis(theta)` respect return typing strategy To accomplish this, also * implements `OneOf` union types * passes desired return typing strategy to behavior factory functions * adds formerly missing BooleanT utils * adds isInteger utility function, along with its dependency isfinite * adds several new and formerly missing Complex utils In addition, it adds several Complex tests, including careful checking of the behavior and return type of `math.cis` under different `math.config.returnTyping` settings. --- src/boolean/all.js | 1 + src/boolean/utils.js | 7 ++++ src/complex/__test__/type.spec.js | 21 +++++++++-- src/complex/__test__/utils.spec.js | 40 +++++++++++++++++++++ src/complex/helpers.js | 7 ++++ src/complex/type.js | 56 +++++++++++++++++++----------- src/complex/utils.js | 14 ++++++-- src/core/Type.js | 21 +++++++++++ src/core/TypeDispatcher.js | 3 +- src/number/__test__/utils.spec.js | 7 ++++ src/number/utils.js | 6 ++++ 11 files changed, 156 insertions(+), 27 deletions(-) create mode 100644 src/boolean/utils.js create mode 100644 src/complex/__test__/utils.spec.js diff --git a/src/boolean/all.js b/src/boolean/all.js index 443a8fa..be70fad 100644 --- a/src/boolean/all.js +++ b/src/boolean/all.js @@ -1,2 +1,3 @@ export * as typeDefinition from './BooleanT.js' export * as type from './type.js' +export * as utilities from './utils.js' diff --git a/src/boolean/utils.js b/src/boolean/utils.js new file mode 100644 index 0000000..ae18ef6 --- /dev/null +++ b/src/boolean/utils.js @@ -0,0 +1,7 @@ +import {BooleanT} from './BooleanT.js' +import {Returns} from '#core/Type.js' +import {match} from '#core/TypePatterns.js' + +export const clone = match(BooleanT, Returns(BooleanT, p => p)) +export const isnan = match(BooleanT, Returns(BooleanT, () => false)) +export const isfinite = match(BooleanT, Returns(BooleanT, () => true)) diff --git a/src/complex/__test__/type.spec.js b/src/complex/__test__/type.spec.js index 6ea0875..2101c74 100644 --- a/src/complex/__test__/type.spec.js +++ b/src/complex/__test__/type.spec.js @@ -1,5 +1,9 @@ import assert from 'assert' import math from '#nanomath' +import {Complex} from '../Complex.js' +import {OneOf, ReturnTyping} from '#core/Type.js' +import {NumberT} from '#number/NumberT.js' + const cplx = math.complex @@ -33,7 +37,20 @@ describe('complex type operations', () => { 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))) + const cis = math.cis.resolve(NumberT) + assert.strictEqual(cis.returns, 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)) + const one = ccis(0) + assert(one !== 1) + assert(math.equal(one, 1)) + assert(math.equal(ccis(Math.PI), cplx(-1))) + math.config.returnTyping = ReturnTyping.free + assert.strictEqual(math.cis.resolve(NumberT), cis) + assert.strictEqual(math.cis(2*Math.PI), 1) }) }) diff --git a/src/complex/__test__/utils.spec.js b/src/complex/__test__/utils.spec.js new file mode 100644 index 0000000..9b88c75 --- /dev/null +++ b/src/complex/__test__/utils.spec.js @@ -0,0 +1,40 @@ +import assert from 'assert' +import math from '#nanomath' + +const cplx = math.complex + +describe('complex utilities', () => { + it('clones a complex', () => { + const z = cplx(3, 4) + const cz = math.clone(z) + assert(cz !== z) + assert.deepStrictEqual(z, cz) + const q = cplx(z, math.add(z, 1)) + const cq = math.clone(q) + assert(cq !== q) + assert.deepStrictEqual(q, cq) + assert(q.re !== cq.re && q.im !== cq.im) + }) + it('checks for nan', () => { + assert(math.isnan(cplx(NaN, NaN))) + assert(!math.isnan(cplx(NaN, 6.28))) + }) + it('tests for finiteness', () => { + const fin = math.isfinite + assert(fin(cplx(3, 4))) + assert(!fin(cplx(2, Infinity))) + assert(!fin(cplx(NaN, 0))) + }) + it('identifies Gaussian integers', () => { + const isInt = math.isInteger + assert(isInt(cplx(3, 4))) + assert(isInt(cplx(-37,0))) + assert(!isInt(cplx(99, -1.000001))) + }) + it('identifies real numbers', () => { + const {isReal} = math + assert(isReal(cplx(5, 0))) + assert(isReal(cplx(5, -1e-17))) + assert(!isReal(cplx(5, 0.000001))) + }) +}) diff --git a/src/complex/helpers.js b/src/complex/helpers.js index 84e828f..5397188 100644 --- a/src/complex/helpers.js +++ b/src/complex/helpers.js @@ -10,6 +10,13 @@ export const promoteUnary = name => match( return ReturnsAs(cplx, z => cplx(compOp(z.re), compOp(z.im))) }) +export const promotePredicateAnd = name => match( + Complex, + (math, C) => { + const compPred = math.resolve(name, C.Component) + return ReturnsAs(compPred, z => compPred(z.re) && compPred(z.im)) + }) + export const promoteBinary = name => match( [Complex, Complex], (math, [W, Z]) => { diff --git a/src/complex/type.js b/src/complex/type.js index e83aabb..e626907 100644 --- a/src/complex/type.js +++ b/src/complex/type.js @@ -1,5 +1,5 @@ import {Complex} from './Complex.js' -import {Returns} from "#core/Type.js" +import {OneOf, Returns, ReturnTyping} from "#core/Type.js" import {Any, match} from "#core/TypePatterns.js" import {BooleanT} from '#boolean/BooleanT.js' import {NumberT} from '#number/NumberT.js' @@ -28,28 +28,42 @@ export const complex = [ ] export const arg = match( - Complex(NumberT), Returns(NumberT, z => Math.atan2(z.im, z.re))) + 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)) - }) + 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)}))) +const _cis = t => ({re: Math.cos(t), im: Math.sin(t)}) + +export const cis = match(NumberT, (math, _type, strategy) => { + if (strategy === ReturnTyping.free) { + const intTest = math.isInteger.resolve(NumberT) + return Returns(OneOf(NumberT, Complex(NumberT)), t => { + let halfCycles = t / Math.PI + if (intTest(halfCycles)) { + halfCycles = Math.round(halfCycles) + return halfCycles % 2 ? -1 : 1 + } + return _cis(t) + }) + } + return Returns(Complex(NumberT), _cis) +}) diff --git a/src/complex/utils.js b/src/complex/utils.js index 2fef03a..77d1acc 100644 --- a/src/complex/utils.js +++ b/src/complex/utils.js @@ -1,9 +1,17 @@ import {Complex} from './Complex.js' import {match} from '#core/TypePatterns.js' import {ReturnsAs} from '#generic/helpers.js' +import {promotePredicateAnd, promoteUnary} from './helpers.js' + +export const clone = promoteUnary('clone') +export const isnan = promotePredicateAnd('isnan') +export const isfinite = promotePredicateAnd('isfinite') +// Note: the followig predicate returns true for all Gaussian integers, not +// just so-called rational integers. +export const isInteger = promotePredicateAnd('isInteger') 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))) + 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 8fd5cb6..398aaaa 100644 --- a/src/core/Type.js +++ b/src/core/Type.js @@ -69,6 +69,27 @@ export const TypeOfTypes = new Type(t => t instanceof Type) export const NotAType = new Type(() => true) // Danger, do not merge! NotAType._doNotMerge = true +const unionDirectory = new ArrayKeyedMap() // make sure only one of each union +export const OneOf = (...types) => { + 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 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( + t => typeList.some(T => T.test(t)), + {typeName: typeList.join('|')})) + } + return unionDirectory.get(typeList) +} + export const Returns = (type, f) => (f.returns = type, f) export const whichType = typs => Returns(TypeOfTypes, item => { diff --git a/src/core/TypeDispatcher.js b/src/core/TypeDispatcher.js index b4da539..d8c6fc1 100644 --- a/src/core/TypeDispatcher.js +++ b/src/core/TypeDispatcher.js @@ -360,7 +360,8 @@ export class TypeDispatcher { try { theBehavior = item( DependencyRecorder(this, '', this, []), - matched(template, this)) + matched(template, this), + strategy) } catch (e) { e.message = `Error in factory for ${key} on ${types} ` + `with return typing ${ReturnTyping.name(strategy)} ` diff --git a/src/number/__test__/utils.spec.js b/src/number/__test__/utils.spec.js index 054a30a..99e11b0 100644 --- a/src/number/__test__/utils.spec.js +++ b/src/number/__test__/utils.spec.js @@ -10,4 +10,11 @@ describe('number utilities', () => { assert.strictEqual(math.isnan(Infinity), false) assert.strictEqual(math.isnan(43), false) }) + it('tests if a number is an integer', () => { + assert(math.isInteger(7)) + assert(math.isInteger(7+5e-16)) + assert(!math.isInteger(7.000001)) + assert(!math.isInteger(-Infinity)) + assert(!math.isInteger(NaN)) + }) }) diff --git a/src/number/utils.js b/src/number/utils.js index c8994c0..7d7f31b 100644 --- a/src/number/utils.js +++ b/src/number/utils.js @@ -4,4 +4,10 @@ import {NumberT} from './NumberT.js' import {match} from '#core/TypePatterns.js' export const clone = plain(a => a) +export const isfinite = match(NumberT, boolnum(isFinite)) +export const isInteger = match(NumberT, math => { + const finite = math.isfinite.resolve(NumberT) + const eq = math.equal.resolve([NumberT, NumberT]) + return boolnum(a => finite(a) && eq(a, Math.round(a)))(math) +}) export const isnan = match(NumberT, boolnum(isNaN))