From c1791ddc20d8e3c36533c5212dbb82ba3d473d5c Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Tue, 15 Apr 2025 20:33:13 -0700 Subject: [PATCH] feat: add nan to types, add hasnan and sign functions Also makes certain to clean up the dependencies that are being generated even in case of throwing an error in the factory for a method. --- src/core/Type.js | 3 +- src/core/TypeDispatcher.js | 62 +++++++++++++------------ src/coretypes/__test__/utils.spec.js | 10 ++++ src/coretypes/utils.js | 21 +++++++++ src/generic/__test__/relational.spec.js | 13 +++++- src/generic/relational.js | 39 +++++++++++++--- src/generic/utils.js | 5 +- src/number/NumberT.js | 3 +- 8 files changed, 114 insertions(+), 42 deletions(-) diff --git a/src/core/Type.js b/src/core/Type.js index d95e5b7..ff07f60 100644 --- a/src/core/Type.js +++ b/src/core/Type.js @@ -4,6 +4,7 @@ export class Type { this.from = options.from ?? {patterns: []} // mock empty Implementations if ('zero' in options) this.zero = options.zero if ('one' in options) this.one = options.one + if ('nan' in options) this.nan = options.nan } toString() { return this.name || `[Type ${this.test}]` @@ -12,7 +13,7 @@ export class Type { export const Undefined = new Type( t => typeof t === 'undefined', - {zero: undefined, one: undefined}) + {zero: undefined, one: undefined, nan: undefined}) export const TypeOfTypes = new Type(t => t instanceof Type) export const NotAType = new Type(t => true) // Danger, do not merge! NotAType._doNotMerge = true diff --git a/src/core/TypeDispatcher.js b/src/core/TypeDispatcher.js index fdcad33..88ed393 100644 --- a/src/core/TypeDispatcher.js +++ b/src/core/TypeDispatcher.js @@ -309,39 +309,41 @@ export class TypeDispatcher { } let theBehavior = () => undefined - this.resolve._genDepsOf.push([key, types]) // Important: make sure - // not to return without popping _genDepsOf - if (!('returns' in item)) { - // looks like a factory - try { - theBehavior = item( - DependencyRecorder(this, '', this, []), - matched(template)) - } catch (e) { - e.message = `Error in factory for ${key} on ${types} ` - + `(match data ${template}): ${e.message}` - throw e - } - } else theBehavior = item + let finalBehavior + this.resolve._genDepsOf.push([key, types]) + 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)) + } catch (e) { + e.message = `Error in factory for ${key} on ${types} ` + + `(match data ${template}): ${e.message}` + throw e + } + } else theBehavior = item - let finalBehavior = theBehavior - if (typeof theBehavior === 'function') { - const returning = theBehavior.returns - if (!returning) { - throw new TypeError( - `No return type specified for ${key} on ${types}`) - } - if (needsCollection(template)) { - // have to wrap the behavior to collect the actual arguments - // in the way corresponding to the template. Generating that - // argument transformer may generate more dependencies. - const morph = this._generateCollectFunction(template) - finalBehavior = - Returns(returning, (...args) => theBehavior(...morph(args))) + finalBehavior = theBehavior + if (typeof theBehavior === 'function') { + const returning = theBehavior.returns + if (!returning) { + throw new TypeError( + `No return type specified for ${key} on ${types}`) + } + if (needsCollection(template)) { + // have to wrap the behavior to collect the actual arguments + // in the way corresponding to the template. Generating that + // argument transformer may generate more dependencies. + const morph = this._generateCollectFunction(template) + finalBehavior = + Returns(returning, (...args) => theBehavior(...morph(args))) + } } + } finally { + this.resolve._genDepsOf.pop() // OK, now it's safe to return } - - this.resolve._genDepsOf.pop() // OK, now it's safe to return behave.set(types, finalBehavior) finalBehavior.template = template return finalBehavior diff --git a/src/coretypes/__test__/utils.spec.js b/src/coretypes/__test__/utils.spec.js index 732bf1f..8fc81f9 100644 --- a/src/coretypes/__test__/utils.spec.js +++ b/src/coretypes/__test__/utils.spec.js @@ -11,4 +11,14 @@ describe('core type utility functions', () => { assert.strictEqual(math.one(undefined), undefined) assert.strictEqual(math.zero(true), false) }) + it('identifies whether types have a NotANumber element', () => { + assert(math.hasnan(math.types.NumberT)) + assert(math.hasnan(73.2)) + assert(math.hasnan(math.types.Undefined)) + assert(math.hasnan(undefined)) + assert(!math.hasnan(math.types.BooleanT)) + assert(!math.hasnan(true)) + assert(isNaN(math.nan(math.types.NumberT))) + assert(isNaN(math.nan(-470.1))) + }) }) diff --git a/src/coretypes/utils.js b/src/coretypes/utils.js index 6bc35f0..dcea51a 100644 --- a/src/coretypes/utils.js +++ b/src/coretypes/utils.js @@ -1,6 +1,7 @@ import {onType} from '#core/helpers.js' import {NotAType, Returns, TypeOfTypes} from '#core/Type.js' import {Any} from "#core/TypePatterns.js" +import {boolnum} from "#number/helpers.js" export const zero = onType( Any, (math, T) => { @@ -24,3 +25,23 @@ export const one = onType( `type '${t}' has no unit element designated as "one"`) }) ) + +export const hasnan = onType( + Any, (math, T) => { + const answer = math.hasnan(T) + return Returns(math.typeOf(answer), () => answer) + }, + TypeOfTypes, boolnum(t => 'nan' in t) +) + +export const nan = onType( + Any, (math, T) => { + const notanum = math.nan(T) + return Returns(T, () => notanum) + }, + TypeOfTypes, Returns(NotAType, t => { + if ('nan' in t) return t.nan + throw new RangeError( + `type '${t}' has no "not a number" element`) + }) +) diff --git a/src/generic/__test__/relational.spec.js b/src/generic/__test__/relational.spec.js index 6506f6a..e7fe77b 100644 --- a/src/generic/__test__/relational.spec.js +++ b/src/generic/__test__/relational.spec.js @@ -45,9 +45,18 @@ describe('generic relational functions', () => { assert.strictEqual(compare(2.2, true), 1) assert.strictEqual(compare(-Infinity, 7), -1) assert(isNaN(compare(NaN, 0))) - assert(isNaN(compare(false, NaN))) + assert.throws(() => compare(false, NaN), TypeError) assert(isNaN(compare(NaN, NaN))) - assert.strictEqual(compare(true, false), 1) + assert.throws(() => compare(true, false), TypeError) assert.throws(() => compare(undefined, -1), ResolutionError) }) + it('determines the sign of numeric values', () => { + const {sign} = math + assert.strictEqual(sign(-8e-16), 0) + assert.strictEqual(sign(Infinity), 1) + assert.strictEqual(sign(-8e-14), -1) + assert(isNaN(sign(NaN))) + assert.throws(() => sign(false), TypeError) + assert.throws(() => sign(undefined), ResolutionError) + }) }) diff --git a/src/generic/relational.js b/src/generic/relational.js index acade73..f47efb7 100644 --- a/src/generic/relational.js +++ b/src/generic/relational.js @@ -3,7 +3,6 @@ import {onType} from '#core/helpers.js' import {Returns} from '#core/Type.js' import {Any, matched} from '#core/TypePatterns.js' import {boolnum} from '#number/helpers.js' -import {NumberT} from '#number/NumberT.js' export const equal = onType([Any, Any], (math, [T, U]) => { // Finding the correct signature of `indistinguishable` to use for @@ -42,11 +41,37 @@ export const equal = onType([Any, Any], (math, [T, U]) => { export const compare = onType([Any, Any], (math, [T, U]) => { const eq = math.equal.resolve([T, U]) const gt = math.exceeds.resolve([T, U]) - const isTnan = math.isnan.resolve([T]) - const isUnan = math.isnan.resolve([U]) - return Returns(NumberT, (t, u) => { - if (isTnan(t) || isUnan(u)) return NaN - if (eq(t,u)) return 0 - return gt(t, u) ? 1 : -1 + const zero = math.zero(T) // asymmetry here is unfortunate, but we have + // to pick some argument's zero to use, so the first argument seemed + // the most reasonable. + const one = math.one(T) + const negOne = math.negate(one) + if (math.typeOf(negOne) !== T) { + throw new TypeError( + `Cannot 'compare()' type '${T}' that has no negative one.`) + } + const hasnanT = math.hasnan(T) + const hasnanU = math.hasnan(U) + if (!hasnanT && !hasnanU) { + return Returns(T, (t, u) => eq(t, u) ? zero : gt(t, u) ? one : negOne) + } + if (hasnanU && !hasnanT) { + throw new TypeError( + `can't compare type ${T} without NaN and type ${U} with NaN`) + } + const isTnan = hasnanT && math.isnan.resolve([T]) + const isUnan = hasnanU && math.isnan.resolve([U]) + const nanT = hasnanT && math.nan(T) + return Returns(T, (t, u) => { + if (hasnanT && isTnan(t)) return nanT + if (hasnanU && isUnan(u)) return nanT // not a typo, stay in T + if (eq(t, u)) return zero + return gt(t, u) ? one : negOne }) }) + +export const sign = onType(Any, (math, T) => { + const zero = math.zero(T) + const comp = math.compare.resolve([T, T]) + return ReturnsAs(comp, t => comp(t, zero)) +}) diff --git a/src/generic/utils.js b/src/generic/utils.js index fc78903..29d9150 100644 --- a/src/generic/utils.js +++ b/src/generic/utils.js @@ -1,9 +1,12 @@ import {ReturnsAs} from './helpers.js' -import {onType} from '#core/helpers.js' +import {onType, ResolutionError} from '#core/helpers.js' import {Returns} from '#core/Type.js' import {Any} from "#core/TypePatterns.js" export const isZero = (math, [T]) => { + if (!T) { // called with no arguments + throw new ResolutionError('isZero() requires one argument') + } const z = math.zero(T) const eq = math.equal.resolve([T, T]) return ReturnsAs(eq, x => eq(z, x)) diff --git a/src/number/NumberT.js b/src/number/NumberT.js index 332fab6..220c28e 100644 --- a/src/number/NumberT.js +++ b/src/number/NumberT.js @@ -5,5 +5,6 @@ import {BooleanT} from '#boolean/BooleanT.js' export const NumberT = new Type(n => typeof n === 'number', { from: onType(BooleanT, math => math.number.resolve([BooleanT])), one: 1, - zero: 0 + zero: 0, + nan: NaN })