feat: add nan to types, add hasnan and sign functions
All checks were successful
/ test (pull_request) Successful in 19s

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.
This commit is contained in:
Glen Whitney 2025-04-15 20:33:13 -07:00
parent 686cd93927
commit c1791ddc20
8 changed files with 114 additions and 42 deletions

View file

@ -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

View file

@ -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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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