feat: config and approximate equality
All checks were successful
/ test (pull_request) Successful in 17s

Establishes a global config object for a TypeDispatcher instance, so far
  with just properties representing comparison tolerances. Begins a
  "relational" group of functions with basic approximate equality, and
  an initial primitive ordering comparison. Ensures that methods that
  depend on properties of `config` will be properly updated when those
  properties change.
This commit is contained in:
Glen Whitney 2025-04-15 01:17:27 -07:00
parent 27fa4b0193
commit d3f2bc09b7
19 changed files with 496 additions and 175 deletions

41
src/core/README.md Normal file
View file

@ -0,0 +1,41 @@
## Nanomath core
The organization here is to keep the core engine as compact and as agnostic
as to what sort of functions and types there might be in a TypeDispatcher as
possible. This division will keep it plausible to break out just the core
as a TypeDispatcher package that could be used independently for any collection
of overloaded functions on a universe of types. So we want to place as few
assumptions/preconditions as to what functions and/or types there will be.
## Core Types
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).
## Core methods
Similarly, as of this writing the only methods that must be in a TypeDispatcher
are:
Type
: the class (constructor) for Type objects, called via `new Type(...)`.
Note that merely constructing a Type does not regeister it within any
TypeDispatcher; it must be `.merge()`d into the TypeDispatcher.
typeOf
: determines the type of any value
merge
: adds values and methods to the TypeDispatcher
resolve
: finds values and methods in the TypeDispatcher, by key and types list
Any (other) functions an instance wants to have acting on the core Types
should be defined elsewhere and merged into the instance.
In nanomath as a whole, rather than within its core, we also assume that
the NumberT type of regular JavaScript numbers is always present (i.e., no
need to check if it is in the instance), and we put all functions that we
want to define on the core Types in the coretypes directory.

View file

@ -1,28 +1,67 @@
import ArrayKeyedMap from 'array-keyed-map'
import {
Implementations, isPlainFunction, isPlainObject, onType
Implementations, ImplementationsGenerator,
isPlainFunction, isPlainObject, onType, types
} from './helpers.js'
import {bootstrapTypes, Returns, whichType, Type} from './Type.js'
import {matched, needsCollection, Passthru} from './TypePatterns.js'
export class TypeDispatcher {
constructor(...specs) {
this._types = {} // stores all the types registered in this dispatcher
this._implementations = {} // maps key to list of [pattern, result] pairs
this._dependencies = {} // maps key to a map from type vectors to...
// ...a set of [key, types] that depend on it.
this._dependencies = new Map() // see explanation below
this._behaviors = {} // maps key to a map from type vectors to results
this._fallbacks = {} // maps key to a catchall result
this.merge({types: () => this._types})
this.merge(bootstrapTypes) // bootstrap the instance
// bootstrap the instance
this.merge({types})
this.merge(bootstrapTypes)
for (const spec of specs) this.merge(spec)
}
/**
* The _dependencies data is the most complicated. It is a map which
* associates, to each item key `ITEM`, a map from type vectors to the
* collection of behaviors that depend on the value associated with
* `ITEM` for that type vector. And here, a collection of behaviors is
* represented by a map from names to maps whose keys are the affected
* type vectors for that name (and whose values are irrelevent).
* Note that the top-level item keys `ITEM` may be direct property names
* of the instance, or `.`-separated paths of property names, the first
* part being the direct property, the second part being a property
* thereof, and so on.
*/
// Only modify a TypeDispatcher via merge! Otherwise dependencies will
// not necessarily be updated properly. Actually I may have it set up so
// that's the only way to modify a TypeDispatcher... not sure, should
// Don't modify a TypeDispatcher except via merge! Otherwise dependencies
// will not necessarily be updated properly. Actually I may have it set
// up so that's the only way to modify a TypeDispatcher... not sure, should
// probably test it.
// There are various things you might want to merge:
// -- a specific entity, to be used as the value for a key for any types
// As long as it's not a plain object, you can do that by just
// merging it. (If it's a direct behavior, rather than a factory, it
// must have its return type labeled.) If it is a plain object,
// you can either merge a factory that produces it, or you can merge
// an Implementations that associates it with the Passthru pattern
// -- a factory for entities, to be invoked to get the value of a key
// for any types. You can just merge that.
// -- a collection of different values, or different behaviors, or
// different factories for different types for a given key. For that
// you merge an Implementations object that associates each item with
// a TypePattern. An Implementation object can most easily be
// generated with `onType(PATTERN, VALUE, PATTERN, VALUE,...)`
// Initially I thought those were all the possibilities. But then I
// wanted to export something that when merged, would set the Passthru
// pattern to a fresh specific object for that merge, but so that the
// identical JavaScript object will be used for all types within that
// particular TypeDispatcher (this situation applies to the config object).
// To produce that behavior, you need a fourth thing
// -- an ImplementationGenerator, which is basically a function that
// returns an Implementations object as above. As this is only needed
// for a single entity that will be merged into multiple different
// TypeDispatchers, there's not a big focus on making this convenient;
// it's not expected to come up much.
merge(spec) {
if (!spec) return
if (typeof spec != 'object') {
@ -31,136 +70,149 @@ export class TypeDispatcher {
}
for (const key in spec) {
let val = spec[key]
// For special cases like types, config, etc, we can wrap
// a function in ImplementationsGenerator to produce the thing
// we should really merge:
if (val instanceof ImplementationsGenerator) {
val = val.generate()
}
// Now dispatch on what sort of thing we are supposed to merge:
if (val instanceof Type) {
// The design here is that we have set up the `types` property to
// be watched like any other, so the following assignment will
// cause any behaviors that depend on the type named `key` in the
// list of types to be cleared out automagically. Seems to work
// so far.
this.types[key] = val
val.name = key
continue
}
if (typeof val === 'function') {
val = onType(Passthru, val)
}
if (val instanceof Implementations) {
if (!(key in this)) {
// Need to "bootstrap" the item:
// We initially define it with a temporary getter, only
// because we don't know whether ultimately it will produce
// a function or a non-callable value, and the "permanent"
// getter we want depends on which it turns out to be. This
// situation means it's not supported to replace a key
// corresponding to a method, after it's been fetched once,
// with a definition that produces a non-callable value;
// the TypeDispatcher will go on returning a function, and
// even if you call that function, it will throw an error
// when it tries to call the non-callable value.
// Conversely, if you try to replace a key corresponding
// to a non-callable value, after it's been fetched once,
// with a function, it will work, but it will not be able to
// perform type dispatch on that function.
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get: () => {
let tryValue
try {
tryValue = this.resolve(key, [])
} catch {
// Has no value for the empty type list, so therefore
// it must be a method, as there is no way to supply
// any types for a non-function value. Hence, we can
// just make tryValue any plain function, since it is
// never actually used, just its type analyzed.
tryValue = () => undefined
}
// Redefine the property according to what sort of
// entity it is:
if (isPlainFunction(tryValue)) {
const thisTypeOf = whichType(this.types)
// the usual case: a method of the dispatcher
const standard = (...args) => {
const types = args.map(thisTypeOf)
return this.resolve(key, types)(...args)
}
standard.resolve = (types) => this.resolve(key, types)
standard.isDispatcher = true
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get: () => standard
})
return standard
}
if (typeof tryValue === 'object') {
if (!('resolve' in tryValue)) {
tryValue.resolve = types => this.resolve(key, types)
}
const get = () => {
if (this._dependencies[key]?.get([])?.size) {
return DependencyWatcher(tryValue, [key], this)
}
return tryValue
}
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get
})
return get()
}
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get: () => tryValue
})
return tryValue
}
})
// Finally, initialize the other data for this key:
this._implementations[key] = []
this._behaviors[key] = new ArrayKeyedMap()
this._dependencies[key] = new ArrayKeyedMap()
}
// Now add all of the patterns of this implementation:
for (const [pattern, result] of val.patterns) {
if (pattern === Passthru) {
if (key in this._fallbacks) this._disengageFallback(key)
this._fallbacks[key] = result
} else {
this._clearBehaviorsMatching(key, pattern)
// if it happens the same pattern is already in the
// implementations, remove it
const imps = this._implementations[key]
const have = imps.findIndex(elt => pattern.equal(elt[0]))
if (have >= 0) imps.splice(have, 1)
this._implementations[key].unshift([pattern, result])
}
}
continue
}
if (isPlainObject(val)) {
if (isPlainObject(val)) { // recurse on subkeys
this.merge(val)
continue
}
// install value as a catchall value
this.merge({[key]: onType(Passthru, val)})
// Everything else we coerce into Implementations and deal with
// right here:
if (!(val instanceof Implementations)) val = onType(Passthru, val)
if (!(key in this)) {
// Need to "bootstrap" the item:
// We initially define it with a temporary getter, only
// because we don't know whether ultimately it will produce
// a function or a non-callable value, and the "permanent"
// getter we want depends on which it turns out to be. This
// situation means it's not supported to replace a key
// corresponding to a method, after it's been fetched once,
// with a definition that produces a non-callable value;
// the TypeDispatcher will go on returning a function, and
// even if you call that function, it will throw an error
// when it tries to call the non-callable value.
// Conversely, if you try to replace a key corresponding
// to a non-callable value, after it's been fetched once,
// with a function, it will work, but it will not be able to
// perform type dispatch on that function.
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get: () => {
let tryValue
try {
tryValue = this.resolve(key, [])
} catch {
// Has no value for the empty type list, so therefore
// it must be a method, as there is no way to supply
// any types for a non-function value. Hence, we can
// just make tryValue any plain function, since it is
// never actually used, just its type analyzed.
tryValue = () => undefined
}
// Redefine the property according to what sort of
// entity it is:
if (isPlainFunction(tryValue)) {
const thisTypeOf = whichType(this.types)
// the usual case: a method of the dispatcher
const standard = (...args) => {
const types = args.map(thisTypeOf)
return this.resolve(key, types)(...args)
}
standard.resolve = (types) => this.resolve(key, types)
standard.isDispatcher = true
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get: () => standard
})
return standard
}
if (typeof tryValue === 'object') {
if (!('resolve' in tryValue)) {
tryValue.resolve = types => this.resolve(key, types)
}
const get = () => {
const keyDeps = this._dependencies.get(key)
if (!keyDeps) return tryValue
const watch = Array.from(keyDeps.keys().filter(
types => types.length === 0
|| this.resolve(key, types) === tryValue
))
if (watch.length) {
return DependencyWatcher(tryValue, key, watch, this)
}
return tryValue
}
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get
})
return get()
}
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get: () => tryValue
})
return tryValue
}
})
// Finally, initialize the other data for this key:
this._implementations[key] = []
this._behaviors[key] = new ArrayKeyedMap()
}
// Now add all of the patterns of this implementation:
for (const [pattern, result] of val.patterns) {
if (pattern === Passthru) {
if (key in this._fallbacks) this._disengageFallback(key)
this._fallbacks[key] = result
} else {
this._clearBehaviorsMatching(key, pattern)
// if it happens the same pattern is already in the
// implementations, remove it
const imps = this._implementations[key]
const have = imps.findIndex(elt => pattern.equal(elt[0]))
if (have >= 0) imps.splice(have, 1)
this._implementations[key].unshift([pattern, result])
}
}
}
}
_addToDeps(key, subkey) {
const depMap = this._dependencies[key]
if (!depMap.has(subkey)) depMap.set(subkey, new Set())
const depSet = depMap.get(subkey)
for (const pair of this.resolve._genDepsOf) depSet.add(pair)
_addToDeps(key, types) {
// Never depend on internal methods:
if (key.startsWith('_')) return
let depMap = this._dependencies.get(key)
if (!depMap) {
depMap = new ArrayKeyedMap()
this._dependencies.set(key, depMap)
}
if (!depMap.has(types)) depMap.set(types, new Map())
const depColl = depMap.get(types)
for (const [dkey, types] of this.resolve._genDepsOf) {
if (!depColl.has(dkey)) depColl.set(dkey, new ArrayKeyedMap())
depColl.get(dkey).set(types, true)
}
}
// produces and returns a function that takes a list of arguments and
@ -191,11 +243,21 @@ export class TypeDispatcher {
throw new ReferenceError(`no method or value for key '${key}'`)
}
if (this.resolve._genDepsOf?.length) this._addToDeps(key, types)
const generatingDeps = this.resolve._genDepsOf?.length
if (generatingDeps) this._addToDeps(key, types)
const behave = this._behaviors[key]
// Return the cached resolution if it's there
if (behave.has(types)) return behave.get(types)
if (behave.has(types)) {
const result = behave.get(types)
if (generatingDeps
&& typeof result === 'object'
&& !(result instanceof Type)
) {
return DependencyRecorder(result, key, this, types)
}
return result
}
// Otherwise, perform the resolution and cache the result
const imps = this._implementations[key]
@ -227,6 +289,12 @@ export class TypeDispatcher {
// If this key is producing a non-function value, we're done
if (!isPlainFunction(item)) {
behave.set(types, item)
if (generatingDeps
&& typeof item === 'object'
&& !(item instanceof Type)
) {
return DependencyRecorder(item, key, this, types)
}
return item
}
@ -244,11 +312,12 @@ export class TypeDispatcher {
// looks like a factory
try {
theBehavior = item(
DependencyRecorder(this, [], this), matched(template))
} catch {
// Oops, didn't work as a factory, so guess we were wrong.
// Just make it the direct value for this key on these types:
theBehavior = item
DependencyRecorder(this, '', this, []),
matched(template))
} catch (e) {
e.message = `Error in factory for ${key} on ${types} `
+ `(match data ${template}): ${e}`
throw e
}
} else theBehavior = item
@ -259,7 +328,7 @@ export class TypeDispatcher {
throw new TypeError(
`No return type specified for ${key} on ${types}`)
}
if (theBehavior.length && needsCollection(template)) {
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.
@ -271,14 +340,20 @@ export class TypeDispatcher {
this.resolve._genDepsOf.pop() // OK, now it's safe to return
behave.set(types, finalBehavior)
finalBehavior.template = template
return finalBehavior
}
// Method called to invalidate a set of behaviors
// Method called to invalidate dependency collection of behaviors
// I think all it has to do is throw them out; they should be
// regenerated properly by resolve, if all is well.
_invalidate(behaveSet) {
for (const [key, types] of behaveSet) this._behaviors[key].delete(types)
_invalidate(depColl) {
if (!depColl) return
for (const [key, typeMap] of depColl) {
for (const types of typeMap.keys()) {
this._behaviors[key].delete(types)
}
}
}
_disengageFallback(key) {
@ -287,7 +362,7 @@ export class TypeDispatcher {
const fallTypes = []
const behs = this._behaviors[key]
const imps = this._implementations[key]
const deps = this._dependencies[key]
const deps = this._dependencies.get(key)
for (const types of behs.keys()) {
let fallsback = true
for (const [pattern] of imps) {
@ -300,8 +375,8 @@ export class TypeDispatcher {
if (fallsback) fallTypes.push(types)
}
for (const types of fallTypes) {
const depSet = deps?.get(types)
if (depSet?.size) this._invalidate(depSet)
const depColl = deps?.get(types)
if (depColl?.size) this._invalidate(depColl)
behs.delete(types)
}
}
@ -309,60 +384,57 @@ export class TypeDispatcher {
_clearBehaviorsMatching(key, pattern) {
// like disengageFallback, just we have the offending pattern:
const behs = this._behaviors[key]
const deps = this._dependencies[key]
const deps = this._dependencies.get(key)
const patTypes = behs.keys().filter(types => {
const [finalIndex] = pattern.match(types)
return finalIndex === types.length
})
for (const types of patTypes) {
const depSet = deps?.get(types)
if (depSet?.size) this._invalidate(depSet)
const depColl = deps?.get(types)
if (depColl?.size) this._invalidate(depColl)
behs.delete(types)
}
}
}
// Proxy that traps accesses and records dependencies on them
const DependencyRecorder = (object, path, repo) => new Proxy(object, {
const DependencyRecorder = (object, path, repo, types) => new Proxy(object, {
get(target, prop, receiver) {
const result = Reflect.get(target, prop, receiver)
// pass resolve calls through, since we record dependencies therein:
if (prop === 'resolve'
// pass internal methods through, as well as resolve calls,
// since we record dependencies within the latter:
if (prop.startsWith('_')
|| prop === 'resolve'
|| (typeof result === 'function' && 'isDispatcher' in result)
) {
return result
}
// OK, it's not a method on a TypeDispatcher, it's some other kind of
// value. So first record the dependency on prop at this path:
const newPath = path.slice()
newPath.push(prop)
const key = newPath[0]
const subpath = newPath.slice(1)
repo._addToDeps(key, subpath)
// value. So first record the dependency on prop at this path.
const newPath = path ? [path, prop].join('.') : prop
repo._addToDeps(newPath, types)
// Now, if the result is an object, we may need to record further
// dependencies on its properties (e.g. math.config.predictable)
// So proxy the return value, except for types, which must maintain
// strict referential identity:
if (typeof result === 'object' && !(result instanceof Type)) {
return DependencyRecorder(result, newPath, repo)
return DependencyRecorder(result, newPath, repo, types)
} else return result
}
})
// The flip side: proxy that traps setting properties and invalidates things
// that depend on them:
const DependencyWatcher = (object, path, repo) => new Proxy(object, {
const DependencyWatcher = (object, path, typesList, repo) => new Proxy(object, {
set(target, prop, value, receiver) {
// First see if this setting has any dependencies:
const newPath = path.slice()
newPath.push(prop)
const key = newPath.shift()
const depSet = repo._dependencies[key]?.get(newPath)
if (depSet?.size) {
// It does. So if we are changing it, invalidate them:
const oldValue = Reflect.get(target, prop, receiver)
if (value !== oldValue) repo._invalidate(depSet)
const newPath = [path, prop].join('.')
const depPerTypes = repo._dependencies.get(newPath)
if (depPerTypes && Reflect.get(target, prop, receiver) !== value) {
for (const types of typesList) {
repo._invalidate(depPerTypes.get(types))
}
}
// Now we can just perform the setting
return Reflect.set(target, prop, value, receiver)
@ -371,9 +443,8 @@ const DependencyWatcher = (object, path, repo) => new Proxy(object, {
// Only thing we need to do is push the watching down
const result = Reflect.get(target, prop, receiver)
if (typeof result === 'object' && !(result instanceof Type)) {
const newPath = path.slice()
newPath.push(prop)
return DependencyWatcher(result, newPath, repo)
const newPath = [path, prop].join('.')
return DependencyWatcher(result, newPath, typesList, repo)
}
return result
}

View file

@ -1,4 +1,4 @@
import {pattern} from './TypePatterns.js'
import {pattern, Passthru} from './TypePatterns.js'
export class Implementations {
constructor(imps) {
@ -17,6 +17,25 @@ export class Implementations {
export const onType = (...imps) => new Implementations(imps)
export class ImplementationsGenerator {
constructor(f) {
this.generate = f
}
}
// the archetypal example of needing an ImplementationsGenerator:
// each TypeDispatcher must have a types property, which will be a
// plain object of types. This must be a different object for each
// TypeDispatcher, but the same object regardless of the types vector
// passed to resolve. So an ordinary factory won't work, because it
// would make a new plain object for each different types vector that
// the property `types` was resolved with. And just a plain object
// wouldn't work, because then every TypeDispatcher would have the same
// collection of types (and modifying the types in one would affect them
// all). Hence we do:
export const types = new ImplementationsGenerator(() => onType(Passthru, {}))
export const isPlainObject = obj => {
if (typeof obj !== 'object') return false
if (!obj) return false // excludes null

9
src/coretypes/README.md Normal file
View file

@ -0,0 +1,9 @@
## Nanomath functions on Core types
The nanomath core is quite parsimonious in terms of what it defines/requires
to be in a TypeDispatcher instance, to make the TypeDispatcher as potentially
flexible as possible for future/other applications, possibly as an independent
package.
As a result, any additional methods on the core types that we would like
to define, such as comparisons between them, are defined in this directory.

View file

@ -0,0 +1,13 @@
import assert from 'assert'
import math from '#nanomath'
const same = math.indistinguishable
describe('core type relational functions', () => {
it('checks if core type entities are the same', () => {
assert(same(undefined, undefined))
assert(same(math.types.Undefined, math.types.Undefined))
assert(!same(math.types.Undefined, math.types.TypeOfTypes))
assert.throws(() => same(undefined, math.Types.Undefined), TypeError)
})
})

1
src/coretypes/all.js Normal file
View file

@ -0,0 +1 @@
export * from './relational.js'

View file

@ -0,0 +1,8 @@
import {onType} from '#core/helpers.js'
import {TypeOfTypes, Undefined} from '#core/Type.js'
import {boolnum} from '#number/helpers.js'
export const indistinguishable = onType(
[Undefined, Undefined], boolnum(() => true),
[TypeOfTypes, TypeOfTypes], boolnum((t, u) => t === u)
)

View file

@ -0,0 +1,41 @@
import assert from 'assert'
import math from '#nanomath'
import * as numbers from '#number/all.js'
import * as generics from '#generic/all.js'
import {TypeDispatcher} from '#core/TypeDispatcher.js'
describe('generic relational functions', () => {
it('tests equality for anything, approx on numbers', () => {
const {equal} = math
assert(equal(undefined, undefined))
assert(equal(math.types.NumberT, math.types.NumberT))
assert(!equal(math.types.NumberT, math.types.BooleanT))
assert(!equal(undefined, math.types.NumberT))
assert(equal(1, 1))
assert(equal(true, 1)) // questionable but same as mathjs
assert(!equal(undefined, true))
assert(equal(1, 1 + 0.9e-12))
assert(equal(0, 1e-16))
assert(!equal(1, 1 + 1.1e-12))
assert(!equal(0, 1.1e-15))
})
it('adjusts equality when config changes', () => {
const jn = new TypeDispatcher(generics, numbers)
const {equal} = jn
assert.strictEqual(equal(1, 1 + 0.9e-12), 1)
assert.strictEqual(equal(0, 1e-16), 1)
assert.strictEqual(equal(1, 1 + 1.1e-12), 0)
assert.strictEqual(equal(0, 1.1e-15), 0)
jn.config.relTol = 1e-10
assert.strictEqual(equal(1, 1 + 1.1e-12), 1)
assert.strictEqual(equal(1, 1 + 1.1e-10), 0)
assert.strictEqual(equal(0, 1.1e-15), 0)
jn.config.absTol = 1e-13
assert.strictEqual(equal(1, 1 + 1.1e-12), 1)
assert.strictEqual(equal(1, 1 + 1.1e-10), 0)
assert.strictEqual(equal(0, 1.1e-15), 1)
assert.strictEqual(equal(0, 1.1e-13), 0)
})
})

View file

@ -1 +1,3 @@
export * as arithmetic from './arithmetic.js'
export * as configuration from './config.js'
export * as relational from './relational.js'

5
src/generic/config.js Normal file
View file

@ -0,0 +1,5 @@
import {ImplementationsGenerator, onType} from '#core/helpers.js'
import {Passthru} from '#core/TypePatterns.js'
export const config = new ImplementationsGenerator(
() => onType(Passthru, {relTol: 1e-12, absTol: 1e-15}))

1
src/generic/helpers.js Normal file
View file

@ -0,0 +1 @@
export const ReturnsAs = (g, f) => (f.returns = g.returns, f)

36
src/generic/relational.js Normal file
View file

@ -0,0 +1,36 @@
import {ReturnsAs} from './helpers.js'
import {onType} from '#core/helpers.js'
import {Any, matched} from '#core/TypePatterns.js'
import {boolnum} from '#number/helpers.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
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
})

View file

@ -1,8 +1,9 @@
import * as booleans from './boolean/all.js'
import * as coretypes from './coretypes/all.js'
import * as generics from './generic/all.js'
import * as numbers from './number/all.js'
import {TypeDispatcher} from '#core/TypeDispatcher.js'
const math = new TypeDispatcher(booleans, generics, numbers)
const math = new TypeDispatcher(booleans, coretypes, generics, numbers)
export default math

View file

@ -0,0 +1,32 @@
import assert from 'assert'
import math from '#nanomath'
const {exceeds, indistinguishable} = math
describe('number relational functions', () => {
it('orders numbers correctly', () => {
assert(exceeds(7, 2.2))
assert(exceeds(0, -1.1))
assert(exceeds(1e-35, 0))
assert(exceeds(Infinity, 1e99))
assert(exceeds(-1e101, -Infinity))
assert(!exceeds(NaN, 0))
assert(!exceeds(0, NaN))
assert(!exceeds(2, 2))
})
it('checks for exact equality', () => {
assert(indistinguishable(0, 0))
assert(indistinguishable(0, -0))
assert(!indistinguishable(0, 1e-35))
assert(!indistinguishable(NaN, NaN))
assert(indistinguishable(Infinity, Infinity))
})
it('checks for approximate equality', () => {
const rel = 1e-12
const abs = 1e-15
assert(indistinguishable(1, 1 + 0.9e-12, rel, abs))
assert(indistinguishable(0, 1e-16, rel, abs))
assert(!indistinguishable(1, 1 + 1.1e-12, rel, abs))
assert(!indistinguishable(0, 1e-14, rel, abs))
})
})

View file

@ -1,4 +1,5 @@
export * as typeDefinition from './NumberT.js'
export * as arithmetic from './arithmetic.js'
export * as relational from './relational.js'
export * as type from './type.js'
export * as utils from './utils.js'

View file

@ -6,4 +6,11 @@ import {Returns} from '#core/Type.js'
export const plain = f => onType(
Array(f.length).fill(NumberT), Returns(NumberT, f))
export const boolnum = Returns(NumberT, p => p ? 1 : 0)
// Takes a behavior returning boolean, and returns a factory
// that returns that behavior if the boolean type is present,
// and otherwise wraps the behavior to return 1 or 0.
export const boolnum = behavior => math => {
const {BooleanT} = math.types
if (BooleanT) return Returns(BooleanT, behavior)
return Returns(NumberT, (...args) => behavior(...args) ? 1 : 0)
}

37
src/number/relational.js Normal file
View file

@ -0,0 +1,37 @@
import {onType} from '#core/helpers.js'
import {Returns} from '#core/Type.js'
import {Optional} from '#core/TypePatterns.js'
import {boolnum} from './helpers.js'
import {NumberT} from './NumberT.js'
// In nanomath, we take the point of view that two comparators are primitive:
// indistinguishable(a, b, relTol, absTol), and exceeds(a, b). All others
// are defined generically in terms of these. They typically return BooleanT,
// but in a numbers-only bundle, they return 1 or 0.
// Notice a feature of TypedDispatcher: if you specify one tolerance, you must
// specify both.
export const indistinguishable = onType(
[NumberT, NumberT, Optional([NumberT, NumberT])],
boolnum((a, b, [tolerances = [0, 0]]) => {
const [relTol, absTol] = tolerances
if (relTol < 0 || absTol < 0) {
throw new RangeError(
`Tolerances (relative: ${relTol}, absolute: ${absTol}) `
+ 'must be nonnegative')
}
if (isNaN(a) || isNaN(b)) return false
if (a === b) return true
if (!isFinite(a) || !isFinite(b)) return false
// |a-b| <= absTol or |a-b| <= relTol*max(|a|, |b|)
const diff = Math.abs(a-b)
if (diff <= absTol) return true
const magnitude = Math.max(Math.abs(a), Math.abs(b))
return diff <= relTol * magnitude
})
)
// Returns truthy if a (interpreted as completely precise) represents a
// greater value than b (interpreted as completely precise). Note that even if
// so, a and b might be indistinguishable() to some tolerances.
export const exceeds = onType([NumberT, NumberT], boolnum((a, b) => a > b))

View file

@ -1,4 +1,4 @@
import {plain, boolnum} from './helpers.js'
import {plain} from './helpers.js'
import {BooleanT} from '#boolean/BooleanT.js'
import {Returns} from '#core/Type.js'
import {NumberT} from '#number/NumberT.js'
@ -7,6 +7,6 @@ const num = f => Returns(NumberT, f)
export const number = plain(a => a)
number.also(
BooleanT, boolnum,
BooleanT, num(p => p ? 1 : 0),
[], num(() => 0)
)

View file

@ -5,8 +5,4 @@ import {Returns} from '#core/Type.js'
import {onType} from '#core/helpers.js'
export const clone = plain(a => a)
export const isnan = onType(NumberT, math => {
const {BooleanT} = math.types
if (BooleanT) return Returns(BooleanT, a => isNaN(a))
return Returns(NumberT, a => boolnum(isNaN(a)))
})
export const isnan = onType(NumberT, boolnum(isNaN))