From d3f2bc09b7747b2b94497ff67bbf5ce84a77400d Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Tue, 15 Apr 2025 01:17:27 -0700 Subject: [PATCH 1/4] feat: config and approximate equality 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. --- src/core/README.md | 41 +++ src/core/TypeDispatcher.js | 401 +++++++++++++--------- src/core/helpers.js | 21 +- src/coretypes/README.md | 9 + src/coretypes/__test__/relational.spec.js | 13 + src/coretypes/all.js | 1 + src/coretypes/relational.js | 8 + src/generic/__test__/relational.spec.js | 41 +++ src/generic/all.js | 2 + src/generic/config.js | 5 + src/generic/helpers.js | 1 + src/generic/relational.js | 36 ++ src/nanomath.js | 3 +- src/number/__test__/relational.spec.js | 32 ++ src/number/all.js | 1 + src/number/helpers.js | 9 +- src/number/relational.js | 37 ++ src/number/type.js | 4 +- src/number/utils.js | 6 +- 19 files changed, 496 insertions(+), 175 deletions(-) create mode 100644 src/core/README.md create mode 100644 src/coretypes/README.md create mode 100644 src/coretypes/__test__/relational.spec.js create mode 100644 src/coretypes/all.js create mode 100644 src/coretypes/relational.js create mode 100644 src/generic/__test__/relational.spec.js create mode 100644 src/generic/config.js create mode 100644 src/generic/helpers.js create mode 100644 src/generic/relational.js create mode 100644 src/number/__test__/relational.spec.js create mode 100644 src/number/relational.js diff --git a/src/core/README.md b/src/core/README.md new file mode 100644 index 0000000..10d3401 --- /dev/null +++ b/src/core/README.md @@ -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. diff --git a/src/core/TypeDispatcher.js b/src/core/TypeDispatcher.js index 64757ce..503b577 100644 --- a/src/core/TypeDispatcher.js +++ b/src/core/TypeDispatcher.js @@ -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 } diff --git a/src/core/helpers.js b/src/core/helpers.js index ce000e5..8ecded5 100644 --- a/src/core/helpers.js +++ b/src/core/helpers.js @@ -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 diff --git a/src/coretypes/README.md b/src/coretypes/README.md new file mode 100644 index 0000000..7ad9187 --- /dev/null +++ b/src/coretypes/README.md @@ -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. diff --git a/src/coretypes/__test__/relational.spec.js b/src/coretypes/__test__/relational.spec.js new file mode 100644 index 0000000..db5e42c --- /dev/null +++ b/src/coretypes/__test__/relational.spec.js @@ -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) + }) +}) diff --git a/src/coretypes/all.js b/src/coretypes/all.js new file mode 100644 index 0000000..ff64f94 --- /dev/null +++ b/src/coretypes/all.js @@ -0,0 +1 @@ +export * from './relational.js' diff --git a/src/coretypes/relational.js b/src/coretypes/relational.js new file mode 100644 index 0000000..3d5712f --- /dev/null +++ b/src/coretypes/relational.js @@ -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) +) diff --git a/src/generic/__test__/relational.spec.js b/src/generic/__test__/relational.spec.js new file mode 100644 index 0000000..5d61b76 --- /dev/null +++ b/src/generic/__test__/relational.spec.js @@ -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) + }) +}) diff --git a/src/generic/all.js b/src/generic/all.js index 0d39ba5..951d1f6 100644 --- a/src/generic/all.js +++ b/src/generic/all.js @@ -1 +1,3 @@ export * as arithmetic from './arithmetic.js' +export * as configuration from './config.js' +export * as relational from './relational.js' diff --git a/src/generic/config.js b/src/generic/config.js new file mode 100644 index 0000000..134d717 --- /dev/null +++ b/src/generic/config.js @@ -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})) diff --git a/src/generic/helpers.js b/src/generic/helpers.js new file mode 100644 index 0000000..976f78d --- /dev/null +++ b/src/generic/helpers.js @@ -0,0 +1 @@ +export const ReturnsAs = (g, f) => (f.returns = g.returns, f) diff --git a/src/generic/relational.js b/src/generic/relational.js new file mode 100644 index 0000000..7e7f92f --- /dev/null +++ b/src/generic/relational.js @@ -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 + }) diff --git a/src/nanomath.js b/src/nanomath.js index 3951ef4..5d4d5be 100644 --- a/src/nanomath.js +++ b/src/nanomath.js @@ -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 diff --git a/src/number/__test__/relational.spec.js b/src/number/__test__/relational.spec.js new file mode 100644 index 0000000..9908482 --- /dev/null +++ b/src/number/__test__/relational.spec.js @@ -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)) + }) +}) diff --git a/src/number/all.js b/src/number/all.js index 1de3c09..7872d78 100644 --- a/src/number/all.js +++ b/src/number/all.js @@ -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' diff --git a/src/number/helpers.js b/src/number/helpers.js index faee3ed..828927c 100644 --- a/src/number/helpers.js +++ b/src/number/helpers.js @@ -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) +} diff --git a/src/number/relational.js b/src/number/relational.js new file mode 100644 index 0000000..87a8152 --- /dev/null +++ b/src/number/relational.js @@ -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)) diff --git a/src/number/type.js b/src/number/type.js index 0c279da..3a9ebfc 100644 --- a/src/number/type.js +++ b/src/number/type.js @@ -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) ) diff --git a/src/number/utils.js b/src/number/utils.js index 06973df..87da6d9 100644 --- a/src/number/utils.js +++ b/src/number/utils.js @@ -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)) -- 2.43.0 From 686cd93927922ce5a7fa8e68556f0faf1bcd01c3 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Tue, 15 Apr 2025 16:23:55 -0700 Subject: [PATCH 2/4] feat: more utility functions 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. --- src/boolean/BooleanT.js | 5 +- src/core/README.md | 7 +++ src/core/Type.js | 8 ++- src/core/TypeDispatcher.js | 37 ++++++------ src/core/__test__/TypeDispatcher.spec.js | 11 +++- src/core/helpers.js | 7 +++ src/coretypes/__test__/utils.spec.js | 14 +++++ src/coretypes/all.js | 1 + src/coretypes/utils.js | 26 ++++++++ src/generic/__test__/relational.spec.js | 12 ++++ src/generic/__test__/utils.spec.js | 13 ++++ src/generic/all.js | 1 + src/generic/relational.js | 76 ++++++++++++++---------- src/generic/utils.js | 10 ++++ src/number/NumberT.js | 2 + src/number/__test__/relational.spec.js | 1 + src/number/type.js | 3 +- 17 files changed, 181 insertions(+), 53 deletions(-) create mode 100644 src/coretypes/__test__/utils.spec.js create mode 100644 src/coretypes/utils.js create mode 100644 src/generic/__test__/utils.spec.js create mode 100644 src/generic/utils.js diff --git a/src/boolean/BooleanT.js b/src/boolean/BooleanT.js index 8d26974..f2792ed 100644 --- a/src/boolean/BooleanT.js +++ b/src/boolean/BooleanT.js @@ -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 +}) diff --git a/src/core/README.md b/src/core/README.md index 10d3401..0cdfd69 100644 --- a/src/core/README.md +++ b/src/core/README.md @@ -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 diff --git a/src/core/Type.js b/src/core/Type.js index 7c728a2..d95e5b7 100644 --- a/src/core/Type.js +++ b/src/core/Type.js @@ -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) diff --git a/src/core/TypeDispatcher.js b/src/core/TypeDispatcher.js index 503b577..fdcad33 100644 --- a/src/core/TypeDispatcher.js +++ b/src/core/TypeDispatcher.js @@ -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 diff --git a/src/core/__test__/TypeDispatcher.spec.js b/src/core/__test__/TypeDispatcher.spec.js index 81d91a3..25ed721 100644 --- a/src/core/__test__/TypeDispatcher.spec.js +++ b/src/core/__test__/TypeDispatcher.spec.js @@ -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) + }) }) diff --git a/src/core/helpers.js b/src/core/helpers.js index 8ecded5..f741859 100644 --- a/src/core/helpers.js +++ b/src/core/helpers.js @@ -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 diff --git a/src/coretypes/__test__/utils.spec.js b/src/coretypes/__test__/utils.spec.js new file mode 100644 index 0000000..732bf1f --- /dev/null +++ b/src/coretypes/__test__/utils.spec.js @@ -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) + }) +}) diff --git a/src/coretypes/all.js b/src/coretypes/all.js index ff64f94..18b23af 100644 --- a/src/coretypes/all.js +++ b/src/coretypes/all.js @@ -1 +1,2 @@ export * from './relational.js' +export * from './utils.js' diff --git a/src/coretypes/utils.js b/src/coretypes/utils.js new file mode 100644 index 0000000..6bc35f0 --- /dev/null +++ b/src/coretypes/utils.js @@ -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"`) + }) +) diff --git a/src/generic/__test__/relational.spec.js b/src/generic/__test__/relational.spec.js index 5d61b76..6506f6a 100644 --- a/src/generic/__test__/relational.spec.js +++ b/src/generic/__test__/relational.spec.js @@ -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) + }) }) diff --git a/src/generic/__test__/utils.spec.js b/src/generic/__test__/utils.spec.js new file mode 100644 index 0000000..e178a2d --- /dev/null +++ b/src/generic/__test__/utils.spec.js @@ -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)) + }) +}) diff --git a/src/generic/all.js b/src/generic/all.js index 951d1f6..4dea4d2 100644 --- a/src/generic/all.js +++ b/src/generic/all.js @@ -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' diff --git a/src/generic/relational.js b/src/generic/relational.js index 7e7f92f..acade73 100644 --- a/src/generic/relational.js +++ b/src/generic/relational.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 + }) +}) diff --git a/src/generic/utils.js b/src/generic/utils.js new file mode 100644 index 0000000..fc78903 --- /dev/null +++ b/src/generic/utils.js @@ -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)) +} diff --git a/src/number/NumberT.js b/src/number/NumberT.js index 99c53ff..332fab6 100644 --- a/src/number/NumberT.js +++ b/src/number/NumberT.js @@ -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 }) diff --git a/src/number/__test__/relational.spec.js b/src/number/__test__/relational.spec.js index 9908482..ce5d8eb 100644 --- a/src/number/__test__/relational.spec.js +++ b/src/number/__test__/relational.spec.js @@ -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', () => { diff --git a/src/number/type.js b/src/number/type.js index 3a9ebfc..9ec8fed 100644 --- a/src/number/type.js +++ b/src/number/type.js @@ -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) ) -- 2.43.0 From c1791ddc20d8e3c36533c5212dbb82ba3d473d5c Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Tue, 15 Apr 2025 20:33:13 -0700 Subject: [PATCH 3/4] 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 }) -- 2.43.0 From 92ac7f38ae440de7a182d14361054bd0254a294c Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Tue, 15 Apr 2025 21:04:22 -0700 Subject: [PATCH 4/4] feat: add the rest of relational from pocomath --- src/generic/__test__/relational.spec.js | 20 +++++++++++++++++ src/generic/relational.js | 29 +++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/src/generic/__test__/relational.spec.js b/src/generic/__test__/relational.spec.js index e7fe77b..18c2f95 100644 --- a/src/generic/__test__/relational.spec.js +++ b/src/generic/__test__/relational.spec.js @@ -59,4 +59,24 @@ describe('generic relational functions', () => { assert.throws(() => sign(false), TypeError) assert.throws(() => sign(undefined), ResolutionError) }) + it('computes inequalities', () => { + const {unequal, larger, largerEq, smaller, smallerEq} = math + assert(!unequal(undefined, undefined)) + assert(!unequal(math.types.NumberT, math.types.NumberT)) + assert(unequal(math.types.NumberT, math.types.BooleanT)) + assert(unequal(undefined, math.types.NumberT)) + assert(!unequal(1, 1)) + assert(!unequal(true, 1)) // questionable but same as mathjs + assert(unequal(undefined, true)) + assert(!unequal(1, 1 + 0.9e-12)) + assert(!unequal(0, 1e-16)) + assert(unequal(1, 1 + 1.1e-12)) + assert(unequal(0, 1.1e-15)) + assert(larger(true, 0.5)) + assert(!larger(3 + 1e-16, 3)) + assert(largerEq(0.5, false)) + assert(largerEq(3 + 1e-16, 3)) + assert(smallerEq(3 + 1e-16, 3)) + assert(!smaller(3, 3 + 1e-16)) + }) }) diff --git a/src/generic/relational.js b/src/generic/relational.js index f47efb7..de5b95b 100644 --- a/src/generic/relational.js +++ b/src/generic/relational.js @@ -75,3 +75,32 @@ export const sign = onType(Any, (math, T) => { const comp = math.compare.resolve([T, T]) return ReturnsAs(comp, t => comp(t, zero)) }) + +export const unequal = (math, types) => { + const eq = math.equal.resolve(types) + return ReturnsAs(eq, (...args) => !eq(...args)) +} + +export const larger = onType([Any, Any], (math, [T, U]) => { + const eq = math.equal.resolve([T, U]) + const bigger = math.exceeds.resolve([T, U]) + return boolnum((t, u) => !eq(t, u) && bigger(t, u))(math) +}) + +export const largerEq = onType([Any, Any], (math, [T, U]) => { + const eq = math.equal.resolve([T, U]) + const bigger = math.exceeds.resolve([T, U]) + return ReturnsAs(bigger, (t, u) => eq(t, u) || bigger(t, u)) +}) + +export const smaller = onType([Any, Any], (math, [T, U]) => { + const eq = math.equal.resolve([T, U]) + const bigger = math.exceeds.resolve([U, T]) + return boolnum((t, u) => !eq(t, u) && bigger(u, t))(math) +}) + +export const smallerEq = onType([Any, Any], (math, [T, U]) => { + const eq = math.equal.resolve([T, U]) + const bigger = math.exceeds.resolve([U, T]) + return ReturnsAs(bigger, (t, u) => eq(t, u) || bigger(u, t)) +}) -- 2.43.0