Adds type constants zero and one, and allows you to obtain them directly from a type object. This facility creates a behavior with a parametric type: the type of `math.zero(T)` where `T` is a Type object (i.e., has type `TypeOfTypes`) depends not just on that type TypeOfTypes, but instead on the _value_ of the argument `T`. Since nanomath is not (yet?) equipped to handle typing such a method, we just set its return type to a new constant NotAType that (hopefully) does not work with the rest of the type system. Also allows you to compute `zero` and `one` from an example value, rather than from the type object itself. Adds utility function `isZero` to test if a value is zero. As usual so far, the additions uncovered some remaining bugs, which this PR fixes. For example, there was a problem in that resolution of the `one` method was failing because the `Any` pattern was blocking matching of the `TypeOfTypes` pattern. Although we may eventually need to sort the patterns for a given method to maintain a reasonable matching order, for now the solution was just to move the two patterns into the same source file and explicitly order them. (With the way onType and Implementations are currently implemented, the proper ordering is more general to more specific, i.e. later implementations supersede earlier ones. Adds many new tests, as always.
This commit is contained in:
parent
d3f2bc09b7
commit
686cd93927
17 changed files with 181 additions and 53 deletions
|
@ -1,3 +1,6 @@
|
|||
import {Type} from '#core/Type.js'
|
||||
|
||||
export const BooleanT = new Type(n => typeof n === 'boolean')
|
||||
export const BooleanT = new Type(n => typeof n === 'boolean', {
|
||||
zero: false,
|
||||
one: true
|
||||
})
|
||||
|
|
|
@ -13,6 +13,13 @@ As of this writing, the only two types required to be in a TypeDispatcher are
|
|||
Undefined (the type inhabited only by `undefined`) and TypeOfTypes (the type
|
||||
inhabited exactly by Type objects).
|
||||
|
||||
There is also a constant NotAType which is the type-world analogue of NaN for
|
||||
numbers. It is occasionally used for the rare behavior that truly does not
|
||||
return any particular type, such as the method `zero` that takes a Type and
|
||||
returns its zero element. However, it does not really work as a Type, and in
|
||||
particular, do _not_ merge it into any TypeDispatcher -- it will disrupt the
|
||||
type and method resolution process.
|
||||
|
||||
## Core methods
|
||||
|
||||
Similarly, as of this writing the only methods that must be in a TypeDispatcher
|
||||
|
|
|
@ -2,14 +2,20 @@ export class Type {
|
|||
constructor(f, options = {}) {
|
||||
this.test = f
|
||||
this.from = options.from ?? {patterns: []} // mock empty Implementations
|
||||
if ('zero' in options) this.zero = options.zero
|
||||
if ('one' in options) this.one = options.one
|
||||
}
|
||||
toString() {
|
||||
return this.name || `[Type ${this.test}]`
|
||||
}
|
||||
}
|
||||
|
||||
export const Undefined = new Type(t => typeof t === 'undefined')
|
||||
export const Undefined = new Type(
|
||||
t => typeof t === 'undefined',
|
||||
{zero: undefined, one: undefined})
|
||||
export const TypeOfTypes = new Type(t => t instanceof Type)
|
||||
export const NotAType = new Type(t => true) // Danger, do not merge!
|
||||
NotAType._doNotMerge = true
|
||||
|
||||
export const Returns = (type, f) => (f.returns = type, f)
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import ArrayKeyedMap from 'array-keyed-map'
|
||||
|
||||
import {
|
||||
Implementations, ImplementationsGenerator,
|
||||
Implementations, ImplementationsGenerator, ResolutionError,
|
||||
isPlainFunction, isPlainObject, onType, types
|
||||
} from './helpers.js'
|
||||
import {bootstrapTypes, Returns, whichType, Type} from './Type.js'
|
||||
|
@ -80,6 +80,9 @@ export class TypeDispatcher {
|
|||
|
||||
// Now dispatch on what sort of thing we are supposed to merge:
|
||||
if (val instanceof Type) {
|
||||
if (val._doNotMerge) {
|
||||
throw new TypeError(`attempt to merge unusable type '${val}'`)
|
||||
}
|
||||
this.types[key] = val
|
||||
val.name = key
|
||||
continue
|
||||
|
@ -219,22 +222,21 @@ export class TypeDispatcher {
|
|||
// transforms them per the given template, to massage them into the form
|
||||
// expected by a behavior associated with the TypePattern that produced
|
||||
// the template.
|
||||
_generateCollectFunction(template, state = {pos: 0}) {
|
||||
const extractors = []
|
||||
for (const elt of template) {
|
||||
if (Array.isArray(elt)) {
|
||||
extractors.push(this._generateCollectFunction(elt, state))
|
||||
} else {
|
||||
const from = state.pos++
|
||||
if ('actual' in elt) { // incorporate conversion
|
||||
let convert = elt.convertor
|
||||
if (!convert.returns) { // it's a factory that produces convert
|
||||
convert = convert(this, elt.actual)
|
||||
}
|
||||
extractors.push(args => convert(args[from]))
|
||||
} else extractors.push(args => args[from])
|
||||
_generateCollectFunction(template, state=false) {
|
||||
if (!Array.isArray(template)) {
|
||||
const from = state ? state.pos++ : 0
|
||||
let extractor = args => args[from]
|
||||
if ('actual' in template) { // incorporate conversion
|
||||
let convert = template.convertor
|
||||
// Check if it's a factory:
|
||||
if (!convert.returns) convert = convert(this, template.actual)
|
||||
extractor = args => convert(args[from])
|
||||
}
|
||||
return state ? extractor : args => [extractor(args)]
|
||||
}
|
||||
state ||= {pos: 0}
|
||||
const extractors = template.map(
|
||||
item => this._generateCollectFunction(item, state))
|
||||
return args => extractors.map(f => f(args))
|
||||
}
|
||||
|
||||
|
@ -284,7 +286,8 @@ export class TypeDispatcher {
|
|||
template = types
|
||||
}
|
||||
if (needItem) {
|
||||
throw new TypeError(`no matching definition of '${key}' on '${types}'`)
|
||||
throw new ResolutionError(
|
||||
`no matching definition of '${key}' on '${types}'`)
|
||||
}
|
||||
// If this key is producing a non-function value, we're done
|
||||
if (!isPlainFunction(item)) {
|
||||
|
@ -316,7 +319,7 @@ export class TypeDispatcher {
|
|||
matched(template))
|
||||
} catch (e) {
|
||||
e.message = `Error in factory for ${key} on ${types} `
|
||||
+ `(match data ${template}): ${e}`
|
||||
+ `(match data ${template}): ${e.message}`
|
||||
throw e
|
||||
}
|
||||
} else theBehavior = item
|
||||
|
|
|
@ -3,9 +3,9 @@ import {TypeDispatcher} from '../TypeDispatcher.js'
|
|||
import * as booleans from '#boolean/all.js'
|
||||
import * as generics from '#generic/all.js'
|
||||
import * as numbers from '#number/all.js'
|
||||
import {onType} from "#core/helpers.js"
|
||||
import {onType, ResolutionError} from "#core/helpers.js"
|
||||
import {Any} from "#core/TypePatterns.js"
|
||||
import {Returns} from "#core/Type.js"
|
||||
import {Returns, NotAType} from "#core/Type.js"
|
||||
import {plain} from "#number/helpers.js"
|
||||
|
||||
describe('TypeDispatcher', () => {
|
||||
|
@ -17,6 +17,7 @@ describe('TypeDispatcher', () => {
|
|||
const {NumberT, TypeOfTypes, Undefined} = incremental.types
|
||||
assert(NumberT.test(7))
|
||||
assert.strictEqual(incremental.add(-1.5, 0.5), -1)
|
||||
assert.throws(() => incremental.add(7, undefined), ResolutionError)
|
||||
// Make Undefined act like zero:
|
||||
incremental.merge({add: onType(
|
||||
[Undefined, Any], (_m, [_U, T]) => Returns(T, (_a, b) => b),
|
||||
|
@ -30,7 +31,7 @@ describe('TypeDispatcher', () => {
|
|||
assert.strictEqual(
|
||||
incremental.add.resolve([Undefined, NumberT]).returns,
|
||||
NumberT)
|
||||
// Oops, changed my mind, make it work like NaN with numbers:
|
||||
// Oops, changed my mind ;-), make it work like NaN with numbers:
|
||||
const alwaysNaN = Returns(NumberT, () => NaN)
|
||||
incremental.merge({add: onType(
|
||||
[Undefined, NumberT], alwaysNaN,
|
||||
|
@ -63,4 +64,8 @@ describe('TypeDispatcher', () => {
|
|||
assert(!bgn._behaviors.negate.has([BooleanT]))
|
||||
assert.strictEqual(bgn.negate(true), -2)
|
||||
})
|
||||
it('disallows merging NotAType', () => {
|
||||
const doomed = new TypeDispatcher()
|
||||
assert.throws(() => doomed.merge({NaT: NotAType}), TypeError)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -36,6 +36,13 @@ export class ImplementationsGenerator {
|
|||
|
||||
export const types = new ImplementationsGenerator(() => onType(Passthru, {}))
|
||||
|
||||
export class ResolutionError extends TypeError {
|
||||
constructor(...args) {
|
||||
super(...args)
|
||||
this.name = 'ResolutionError'
|
||||
}
|
||||
}
|
||||
|
||||
export const isPlainObject = obj => {
|
||||
if (typeof obj !== 'object') return false
|
||||
if (!obj) return false // excludes null
|
||||
|
|
14
src/coretypes/__test__/utils.spec.js
Normal file
14
src/coretypes/__test__/utils.spec.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
import assert from 'assert'
|
||||
import math from '#nanomath'
|
||||
|
||||
describe('core type utility functions', () => {
|
||||
it('identifies zero and one elements in most types', () => {
|
||||
assert.strictEqual(math.zero(math.types.NumberT), 0)
|
||||
assert.strictEqual(math.zero(math.types.Undefined), undefined)
|
||||
assert.strictEqual(math.one(math.types.BooleanT), true)
|
||||
assert.throws(() => math.one(math.types.TypeOfTypes), RangeError)
|
||||
assert.strictEqual(math.one(-7.5), 1)
|
||||
assert.strictEqual(math.one(undefined), undefined)
|
||||
assert.strictEqual(math.zero(true), false)
|
||||
})
|
||||
})
|
|
@ -1 +1,2 @@
|
|||
export * from './relational.js'
|
||||
export * from './utils.js'
|
||||
|
|
26
src/coretypes/utils.js
Normal file
26
src/coretypes/utils.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
import {onType} from '#core/helpers.js'
|
||||
import {NotAType, Returns, TypeOfTypes} from '#core/Type.js'
|
||||
import {Any} from "#core/TypePatterns.js"
|
||||
|
||||
export const zero = onType(
|
||||
Any, (math, T) => {
|
||||
const z = math.zero(T)
|
||||
return Returns(T, () => z)
|
||||
},
|
||||
TypeOfTypes, Returns(NotAType, t => {
|
||||
if ('zero' in t) return t.zero
|
||||
throw new RangeError(`type '${t}' has no zero element`)
|
||||
})
|
||||
)
|
||||
|
||||
export const one = onType(
|
||||
Any, (math, T) => {
|
||||
const unit = math.one(T)
|
||||
return Returns(T, () => unit)
|
||||
},
|
||||
TypeOfTypes, Returns(NotAType, t => {
|
||||
if ('one' in t) return t.one
|
||||
throw new RangeError(
|
||||
`type '${t}' has no unit element designated as "one"`)
|
||||
})
|
||||
)
|
|
@ -2,6 +2,7 @@ import assert from 'assert'
|
|||
import math from '#nanomath'
|
||||
import * as numbers from '#number/all.js'
|
||||
import * as generics from '#generic/all.js'
|
||||
import {ResolutionError} from '#core/helpers.js'
|
||||
import {TypeDispatcher} from '#core/TypeDispatcher.js'
|
||||
|
||||
describe('generic relational functions', () => {
|
||||
|
@ -38,4 +39,15 @@ describe('generic relational functions', () => {
|
|||
assert.strictEqual(equal(0, 1.1e-15), 1)
|
||||
assert.strictEqual(equal(0, 1.1e-13), 0)
|
||||
})
|
||||
it('performs three-way comparison', () => {
|
||||
const {compare} = math
|
||||
assert.strictEqual(compare(-0.4e-15, +0.4e-15), 0)
|
||||
assert.strictEqual(compare(2.2, true), 1)
|
||||
assert.strictEqual(compare(-Infinity, 7), -1)
|
||||
assert(isNaN(compare(NaN, 0)))
|
||||
assert(isNaN(compare(false, NaN)))
|
||||
assert(isNaN(compare(NaN, NaN)))
|
||||
assert.strictEqual(compare(true, false), 1)
|
||||
assert.throws(() => compare(undefined, -1), ResolutionError)
|
||||
})
|
||||
})
|
||||
|
|
13
src/generic/__test__/utils.spec.js
Normal file
13
src/generic/__test__/utils.spec.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
import assert from 'assert'
|
||||
import math from '#nanomath'
|
||||
|
||||
describe('generic utility functions', () => {
|
||||
it('tests whether an element is zero', () => {
|
||||
const {isZero} = math
|
||||
assert(!isZero(3))
|
||||
assert(isZero(3e-16))
|
||||
assert(isZero(false))
|
||||
assert(!isZero(true))
|
||||
assert(isZero(undefined))
|
||||
})
|
||||
})
|
|
@ -1,3 +1,4 @@
|
|||
export * as arithmetic from './arithmetic.js'
|
||||
export * as configuration from './config.js'
|
||||
export * as relational from './relational.js'
|
||||
export * as utilities from './utils.js'
|
||||
|
|
|
@ -1,36 +1,52 @@
|
|||
import {ReturnsAs} from './helpers.js'
|
||||
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
|
||||
// testing (approximate) equality is tricky, because T or U might
|
||||
// need to be converted for the sake of comparison, and some types
|
||||
// allow tolerances for equality and others don't. So the plan is
|
||||
// we first look up without tolerances, then we check the config for
|
||||
// the matching type, and then we look up with tolerances.
|
||||
let exactChecker
|
||||
export const equal = onType([Any, Any], (math, [T, U]) => {
|
||||
// Finding the correct signature of `indistinguishable` to use for
|
||||
// testing (approximate) equality is tricky, because T or U might
|
||||
// need to be converted for the sake of comparison, and some types
|
||||
// allow tolerances for equality and others don't. So the plan is
|
||||
// we first look up without tolerances, then we check the config for
|
||||
// the matching type, and then we look up with tolerances.
|
||||
let exactChecker
|
||||
try {
|
||||
exactChecker = math.indistinguishable.resolve([T, U])
|
||||
} catch { // can't compare, so no way they can be equal
|
||||
return boolnum(() => false)(math)
|
||||
}
|
||||
// Get the type of the first argument to the matching checker:
|
||||
const ByType = matched(exactChecker.template).flat()[0]
|
||||
// Now see if there are tolerances for that type:
|
||||
const typeConfig = math.resolve('config', [ByType])
|
||||
if ('relTol' in typeConfig) {
|
||||
try {
|
||||
exactChecker = math.indistinguishable.resolve([T, U])
|
||||
} catch { // can't compare, so no way they can be equal
|
||||
return boolnum(() => false)(math)
|
||||
}
|
||||
// Get the type of the first argument to the matching checker:
|
||||
const ByType = matched(exactChecker.template).flat()[0]
|
||||
// Now see if there are tolerances for that type:
|
||||
const typeConfig = math.resolve('config', [ByType])
|
||||
if ('relTol' in typeConfig) {
|
||||
try {
|
||||
const {relTol, absTol} = typeConfig
|
||||
const RT = math.typeOf(relTol)
|
||||
const AT = math.typeOf(absTol)
|
||||
const approx = math.indistinguishable.resolve([T, U, RT, AT])
|
||||
return ReturnsAs(
|
||||
approx, (t, u) => approx(t, u, relTol, absTol))
|
||||
} catch {} // fall through to case with no tolerances
|
||||
}
|
||||
// either no tolerances or no matching signature for indistinguishable
|
||||
return exactChecker
|
||||
})
|
||||
const {relTol, absTol} = typeConfig
|
||||
const RT = math.typeOf(relTol)
|
||||
const AT = math.typeOf(absTol)
|
||||
const approx = math.indistinguishable.resolve([T, U, RT, AT])
|
||||
return ReturnsAs(
|
||||
approx, (t, u) => approx(t, u, relTol, absTol))
|
||||
} catch {} // fall through to case with no tolerances
|
||||
}
|
||||
// either no tolerances or no matching signature for indistinguishable
|
||||
return exactChecker
|
||||
})
|
||||
|
||||
// now that we have `equal` and `exceeds`, pretty much everything else should
|
||||
// be easy:
|
||||
|
||||
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
|
||||
})
|
||||
})
|
||||
|
|
10
src/generic/utils.js
Normal file
10
src/generic/utils.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
import {ReturnsAs} from './helpers.js'
|
||||
import {onType} from '#core/helpers.js'
|
||||
import {Returns} from '#core/Type.js'
|
||||
import {Any} from "#core/TypePatterns.js"
|
||||
|
||||
export const isZero = (math, [T]) => {
|
||||
const z = math.zero(T)
|
||||
const eq = math.equal.resolve([T, T])
|
||||
return ReturnsAs(eq, x => eq(z, x))
|
||||
}
|
|
@ -4,4 +4,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
|
||||
})
|
||||
|
|
|
@ -12,6 +12,7 @@ describe('number relational functions', () => {
|
|||
assert(exceeds(-1e101, -Infinity))
|
||||
assert(!exceeds(NaN, 0))
|
||||
assert(!exceeds(0, NaN))
|
||||
assert(!exceeds(NaN, NaN))
|
||||
assert(!exceeds(2, 2))
|
||||
})
|
||||
it('checks for exact equality', () => {
|
||||
|
|
|
@ -7,6 +7,7 @@ const num = f => Returns(NumberT, f)
|
|||
|
||||
export const number = plain(a => a)
|
||||
number.also(
|
||||
BooleanT, num(p => p ? 1 : 0),
|
||||
// conversions from Boolean should be consistent with one and zero:
|
||||
BooleanT, num(p => p ? NumberT.one : NumberT.zero),
|
||||
[], num(() => 0)
|
||||
)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue