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 })