From 70ce01d12b86b0f4fb61cdb8dee50f13d50d4d65 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Wed, 16 Apr 2025 04:23:48 +0000 Subject: [PATCH] feat: config and approximate equality (#19) 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. Reviewed-on: https://code.studioinfinity.org/StudioInfinity/nanomath/pulls/19 Co-authored-by: Glen Whitney Co-committed-by: Glen Whitney --- src/boolean/BooleanT.js | 5 +- src/core/README.md | 48 +++ src/core/Type.js | 9 +- src/core/TypeDispatcher.js | 482 +++++++++++++--------- src/core/__test__/TypeDispatcher.spec.js | 11 +- src/core/helpers.js | 28 +- src/coretypes/README.md | 9 + src/coretypes/__test__/relational.spec.js | 13 + src/coretypes/__test__/utils.spec.js | 24 ++ src/coretypes/all.js | 2 + src/coretypes/relational.js | 8 + src/coretypes/utils.js | 47 +++ src/generic/__test__/relational.spec.js | 82 ++++ src/generic/__test__/utils.spec.js | 13 + src/generic/all.js | 3 + src/generic/config.js | 5 + src/generic/helpers.js | 1 + src/generic/relational.js | 106 +++++ src/generic/utils.js | 13 + src/nanomath.js | 3 +- src/number/NumberT.js | 3 + src/number/__test__/relational.spec.js | 33 ++ src/number/all.js | 1 + src/number/helpers.js | 9 +- src/number/relational.js | 37 ++ src/number/type.js | 5 +- src/number/utils.js | 6 +- 27 files changed, 788 insertions(+), 218 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/__test__/utils.spec.js create mode 100644 src/coretypes/all.js create mode 100644 src/coretypes/relational.js create mode 100644 src/coretypes/utils.js create mode 100644 src/generic/__test__/relational.spec.js create mode 100644 src/generic/__test__/utils.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/generic/utils.js create mode 100644 src/number/__test__/relational.spec.js create mode 100644 src/number/relational.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 new file mode 100644 index 0000000..0cdfd69 --- /dev/null +++ b/src/core/README.md @@ -0,0 +1,48 @@ +## 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). + +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 +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/Type.js b/src/core/Type.js index 7c728a2..ff07f60 100644 --- a/src/core/Type.js +++ b/src/core/Type.js @@ -2,14 +2,21 @@ 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 + if ('nan' in options) this.nan = options.nan } 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, 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 export const Returns = (type, f) => (f.returns = type, f) diff --git a/src/core/TypeDispatcher.js b/src/core/TypeDispatcher.js index 64757ce..88ed393 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, ResolutionError, + 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,158 +70,173 @@ 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. + if (val._doNotMerge) { + throw new TypeError(`attempt to merge unusable type '${val}'`) + } 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 // 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)) } @@ -191,11 +245,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] @@ -222,11 +286,18 @@ 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)) { behave.set(types, item) + if (generatingDeps + && typeof item === 'object' + && !(item instanceof Type) + ) { + return DependencyRecorder(item, key, this, types) + } return item } @@ -238,47 +309,56 @@ 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 { - // 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 - } - } 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 (theBehavior.length && 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 } - // 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 +367,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 +380,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 +389,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 +448,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/__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 ce000e5..f741859 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,32 @@ 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 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/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/__test__/utils.spec.js b/src/coretypes/__test__/utils.spec.js new file mode 100644 index 0000000..8fc81f9 --- /dev/null +++ b/src/coretypes/__test__/utils.spec.js @@ -0,0 +1,24 @@ +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) + }) + 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/all.js b/src/coretypes/all.js new file mode 100644 index 0000000..18b23af --- /dev/null +++ b/src/coretypes/all.js @@ -0,0 +1,2 @@ +export * from './relational.js' +export * from './utils.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/coretypes/utils.js b/src/coretypes/utils.js new file mode 100644 index 0000000..dcea51a --- /dev/null +++ b/src/coretypes/utils.js @@ -0,0 +1,47 @@ +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) => { + 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"`) + }) +) + +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 new file mode 100644 index 0000000..18c2f95 --- /dev/null +++ b/src/generic/__test__/relational.spec.js @@ -0,0 +1,82 @@ +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', () => { + 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) + }) + 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.throws(() => compare(false, NaN), TypeError) + assert(isNaN(compare(NaN, NaN))) + 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) + }) + 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/__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 0d39ba5..4dea4d2 100644 --- a/src/generic/all.js +++ b/src/generic/all.js @@ -1 +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/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..de5b95b --- /dev/null +++ b/src/generic/relational.js @@ -0,0 +1,106 @@ +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' + +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 +}) + +// 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 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)) +}) + +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)) +}) diff --git a/src/generic/utils.js b/src/generic/utils.js new file mode 100644 index 0000000..29d9150 --- /dev/null +++ b/src/generic/utils.js @@ -0,0 +1,13 @@ +import {ReturnsAs} from './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/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/NumberT.js b/src/number/NumberT.js index 99c53ff..220c28e 100644 --- a/src/number/NumberT.js +++ b/src/number/NumberT.js @@ -4,4 +4,7 @@ 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, + nan: NaN }) diff --git a/src/number/__test__/relational.spec.js b/src/number/__test__/relational.spec.js new file mode 100644 index 0000000..ce5d8eb --- /dev/null +++ b/src/number/__test__/relational.spec.js @@ -0,0 +1,33 @@ +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(NaN, 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..9ec8fed 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,7 @@ const num = f => Returns(NumberT, f) export const number = plain(a => a) number.also( - BooleanT, boolnum, + // conversions from Boolean should be consistent with one and zero: + BooleanT, num(p => p ? NumberT.one : NumberT.zero), [], 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))