feat: config and approximate equality
All checks were successful
/ test (pull_request) Successful in 17s
All checks were successful
/ test (pull_request) Successful in 17s
Establishes a global config object for a TypeDispatcher instance, so far with just properties representing comparison tolerances. Begins a "relational" group of functions with basic approximate equality, and an initial primitive ordering comparison. Ensures that methods that depend on properties of `config` will be properly updated when those properties change.
This commit is contained in:
parent
27fa4b0193
commit
d3f2bc09b7
19 changed files with 496 additions and 175 deletions
41
src/core/README.md
Normal file
41
src/core/README.md
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
## Nanomath core
|
||||||
|
|
||||||
|
The organization here is to keep the core engine as compact and as agnostic
|
||||||
|
as to what sort of functions and types there might be in a TypeDispatcher as
|
||||||
|
possible. This division will keep it plausible to break out just the core
|
||||||
|
as a TypeDispatcher package that could be used independently for any collection
|
||||||
|
of overloaded functions on a universe of types. So we want to place as few
|
||||||
|
assumptions/preconditions as to what functions and/or types there will be.
|
||||||
|
|
||||||
|
## Core Types
|
||||||
|
|
||||||
|
As of this writing, the only two types required to be in a TypeDispatcher are
|
||||||
|
Undefined (the type inhabited only by `undefined`) and TypeOfTypes (the type
|
||||||
|
inhabited exactly by Type objects).
|
||||||
|
|
||||||
|
## Core methods
|
||||||
|
|
||||||
|
Similarly, as of this writing the only methods that must be in a TypeDispatcher
|
||||||
|
are:
|
||||||
|
|
||||||
|
Type
|
||||||
|
: the class (constructor) for Type objects, called via `new Type(...)`.
|
||||||
|
Note that merely constructing a Type does not regeister it within any
|
||||||
|
TypeDispatcher; it must be `.merge()`d into the TypeDispatcher.
|
||||||
|
|
||||||
|
typeOf
|
||||||
|
: determines the type of any value
|
||||||
|
|
||||||
|
merge
|
||||||
|
: adds values and methods to the TypeDispatcher
|
||||||
|
|
||||||
|
resolve
|
||||||
|
: finds values and methods in the TypeDispatcher, by key and types list
|
||||||
|
|
||||||
|
Any (other) functions an instance wants to have acting on the core Types
|
||||||
|
should be defined elsewhere and merged into the instance.
|
||||||
|
|
||||||
|
In nanomath as a whole, rather than within its core, we also assume that
|
||||||
|
the NumberT type of regular JavaScript numbers is always present (i.e., no
|
||||||
|
need to check if it is in the instance), and we put all functions that we
|
||||||
|
want to define on the core Types in the coretypes directory.
|
|
@ -1,28 +1,67 @@
|
||||||
import ArrayKeyedMap from 'array-keyed-map'
|
import ArrayKeyedMap from 'array-keyed-map'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Implementations, isPlainFunction, isPlainObject, onType
|
Implementations, ImplementationsGenerator,
|
||||||
|
isPlainFunction, isPlainObject, onType, types
|
||||||
} from './helpers.js'
|
} from './helpers.js'
|
||||||
import {bootstrapTypes, Returns, whichType, Type} from './Type.js'
|
import {bootstrapTypes, Returns, whichType, Type} from './Type.js'
|
||||||
import {matched, needsCollection, Passthru} from './TypePatterns.js'
|
import {matched, needsCollection, Passthru} from './TypePatterns.js'
|
||||||
|
|
||||||
export class TypeDispatcher {
|
export class TypeDispatcher {
|
||||||
constructor(...specs) {
|
constructor(...specs) {
|
||||||
this._types = {} // stores all the types registered in this dispatcher
|
|
||||||
this._implementations = {} // maps key to list of [pattern, result] pairs
|
this._implementations = {} // maps key to list of [pattern, result] pairs
|
||||||
this._dependencies = {} // maps key to a map from type vectors to...
|
this._dependencies = new Map() // see explanation below
|
||||||
// ...a set of [key, types] that depend on it.
|
|
||||||
this._behaviors = {} // maps key to a map from type vectors to results
|
this._behaviors = {} // maps key to a map from type vectors to results
|
||||||
this._fallbacks = {} // maps key to a catchall result
|
this._fallbacks = {} // maps key to a catchall result
|
||||||
this.merge({types: () => this._types})
|
// bootstrap the instance
|
||||||
this.merge(bootstrapTypes) // bootstrap the instance
|
this.merge({types})
|
||||||
|
this.merge(bootstrapTypes)
|
||||||
for (const spec of specs) this.merge(spec)
|
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
|
// Don't modify a TypeDispatcher except via merge! Otherwise dependencies
|
||||||
// not necessarily be updated properly. Actually I may have it set up so
|
// will not necessarily be updated properly. Actually I may have it set
|
||||||
// that's the only way to modify a TypeDispatcher... not sure, should
|
// up so that's the only way to modify a TypeDispatcher... not sure, should
|
||||||
// probably test it.
|
// 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) {
|
merge(spec) {
|
||||||
if (!spec) return
|
if (!spec) return
|
||||||
if (typeof spec != 'object') {
|
if (typeof spec != 'object') {
|
||||||
|
@ -31,136 +70,149 @@ export class TypeDispatcher {
|
||||||
}
|
}
|
||||||
for (const key in spec) {
|
for (const key in spec) {
|
||||||
let val = spec[key]
|
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) {
|
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
|
this.types[key] = val
|
||||||
val.name = key
|
val.name = key
|
||||||
continue
|
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 (isPlainObject(val)) { // recurse on subkeys
|
||||||
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)) {
|
|
||||||
this.merge(val)
|
this.merge(val)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// install value as a catchall value
|
// Everything else we coerce into Implementations and deal with
|
||||||
this.merge({[key]: onType(Passthru, val)})
|
// 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) {
|
_addToDeps(key, types) {
|
||||||
const depMap = this._dependencies[key]
|
// Never depend on internal methods:
|
||||||
if (!depMap.has(subkey)) depMap.set(subkey, new Set())
|
if (key.startsWith('_')) return
|
||||||
const depSet = depMap.get(subkey)
|
let depMap = this._dependencies.get(key)
|
||||||
for (const pair of this.resolve._genDepsOf) depSet.add(pair)
|
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
|
// 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}'`)
|
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]
|
const behave = this._behaviors[key]
|
||||||
// Return the cached resolution if it's there
|
// 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
|
// Otherwise, perform the resolution and cache the result
|
||||||
const imps = this._implementations[key]
|
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 this key is producing a non-function value, we're done
|
||||||
if (!isPlainFunction(item)) {
|
if (!isPlainFunction(item)) {
|
||||||
behave.set(types, item)
|
behave.set(types, item)
|
||||||
|
if (generatingDeps
|
||||||
|
&& typeof item === 'object'
|
||||||
|
&& !(item instanceof Type)
|
||||||
|
) {
|
||||||
|
return DependencyRecorder(item, key, this, types)
|
||||||
|
}
|
||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -244,11 +312,12 @@ export class TypeDispatcher {
|
||||||
// looks like a factory
|
// looks like a factory
|
||||||
try {
|
try {
|
||||||
theBehavior = item(
|
theBehavior = item(
|
||||||
DependencyRecorder(this, [], this), matched(template))
|
DependencyRecorder(this, '', this, []),
|
||||||
} catch {
|
matched(template))
|
||||||
// Oops, didn't work as a factory, so guess we were wrong.
|
} catch (e) {
|
||||||
// Just make it the direct value for this key on these types:
|
e.message = `Error in factory for ${key} on ${types} `
|
||||||
theBehavior = item
|
+ `(match data ${template}): ${e}`
|
||||||
|
throw e
|
||||||
}
|
}
|
||||||
} else theBehavior = item
|
} else theBehavior = item
|
||||||
|
|
||||||
|
@ -259,7 +328,7 @@ export class TypeDispatcher {
|
||||||
throw new TypeError(
|
throw new TypeError(
|
||||||
`No return type specified for ${key} on ${types}`)
|
`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
|
// have to wrap the behavior to collect the actual arguments
|
||||||
// in the way corresponding to the template. Generating that
|
// in the way corresponding to the template. Generating that
|
||||||
// argument transformer may generate more dependencies.
|
// argument transformer may generate more dependencies.
|
||||||
|
@ -271,14 +340,20 @@ export class TypeDispatcher {
|
||||||
|
|
||||||
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)
|
behave.set(types, finalBehavior)
|
||||||
|
finalBehavior.template = template
|
||||||
return finalBehavior
|
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
|
// I think all it has to do is throw them out; they should be
|
||||||
// regenerated properly by resolve, if all is well.
|
// regenerated properly by resolve, if all is well.
|
||||||
_invalidate(behaveSet) {
|
_invalidate(depColl) {
|
||||||
for (const [key, types] of behaveSet) this._behaviors[key].delete(types)
|
if (!depColl) return
|
||||||
|
for (const [key, typeMap] of depColl) {
|
||||||
|
for (const types of typeMap.keys()) {
|
||||||
|
this._behaviors[key].delete(types)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_disengageFallback(key) {
|
_disengageFallback(key) {
|
||||||
|
@ -287,7 +362,7 @@ export class TypeDispatcher {
|
||||||
const fallTypes = []
|
const fallTypes = []
|
||||||
const behs = this._behaviors[key]
|
const behs = this._behaviors[key]
|
||||||
const imps = this._implementations[key]
|
const imps = this._implementations[key]
|
||||||
const deps = this._dependencies[key]
|
const deps = this._dependencies.get(key)
|
||||||
for (const types of behs.keys()) {
|
for (const types of behs.keys()) {
|
||||||
let fallsback = true
|
let fallsback = true
|
||||||
for (const [pattern] of imps) {
|
for (const [pattern] of imps) {
|
||||||
|
@ -300,8 +375,8 @@ export class TypeDispatcher {
|
||||||
if (fallsback) fallTypes.push(types)
|
if (fallsback) fallTypes.push(types)
|
||||||
}
|
}
|
||||||
for (const types of fallTypes) {
|
for (const types of fallTypes) {
|
||||||
const depSet = deps?.get(types)
|
const depColl = deps?.get(types)
|
||||||
if (depSet?.size) this._invalidate(depSet)
|
if (depColl?.size) this._invalidate(depColl)
|
||||||
behs.delete(types)
|
behs.delete(types)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -309,60 +384,57 @@ export class TypeDispatcher {
|
||||||
_clearBehaviorsMatching(key, pattern) {
|
_clearBehaviorsMatching(key, pattern) {
|
||||||
// like disengageFallback, just we have the offending pattern:
|
// like disengageFallback, just we have the offending pattern:
|
||||||
const behs = this._behaviors[key]
|
const behs = this._behaviors[key]
|
||||||
const deps = this._dependencies[key]
|
const deps = this._dependencies.get(key)
|
||||||
const patTypes = behs.keys().filter(types => {
|
const patTypes = behs.keys().filter(types => {
|
||||||
const [finalIndex] = pattern.match(types)
|
const [finalIndex] = pattern.match(types)
|
||||||
return finalIndex === types.length
|
return finalIndex === types.length
|
||||||
})
|
})
|
||||||
for (const types of patTypes) {
|
for (const types of patTypes) {
|
||||||
const depSet = deps?.get(types)
|
const depColl = deps?.get(types)
|
||||||
if (depSet?.size) this._invalidate(depSet)
|
if (depColl?.size) this._invalidate(depColl)
|
||||||
behs.delete(types)
|
behs.delete(types)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Proxy that traps accesses and records dependencies on them
|
// 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) {
|
get(target, prop, receiver) {
|
||||||
const result = Reflect.get(target, prop, receiver)
|
const result = Reflect.get(target, prop, receiver)
|
||||||
// pass resolve calls through, since we record dependencies therein:
|
// pass internal methods through, as well as resolve calls,
|
||||||
if (prop === 'resolve'
|
// since we record dependencies within the latter:
|
||||||
|
if (prop.startsWith('_')
|
||||||
|
|| prop === 'resolve'
|
||||||
|| (typeof result === 'function' && 'isDispatcher' in result)
|
|| (typeof result === 'function' && 'isDispatcher' in result)
|
||||||
) {
|
) {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// OK, it's not a method on a TypeDispatcher, it's some other kind of
|
// 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:
|
// value. So first record the dependency on prop at this path.
|
||||||
const newPath = path.slice()
|
const newPath = path ? [path, prop].join('.') : prop
|
||||||
newPath.push(prop)
|
repo._addToDeps(newPath, types)
|
||||||
const key = newPath[0]
|
|
||||||
const subpath = newPath.slice(1)
|
|
||||||
repo._addToDeps(key, subpath)
|
|
||||||
// Now, if the result is an object, we may need to record further
|
// Now, if the result is an object, we may need to record further
|
||||||
// dependencies on its properties (e.g. math.config.predictable)
|
// dependencies on its properties (e.g. math.config.predictable)
|
||||||
// So proxy the return value, except for types, which must maintain
|
// So proxy the return value, except for types, which must maintain
|
||||||
// strict referential identity:
|
// strict referential identity:
|
||||||
if (typeof result === 'object' && !(result instanceof Type)) {
|
if (typeof result === 'object' && !(result instanceof Type)) {
|
||||||
return DependencyRecorder(result, newPath, repo)
|
return DependencyRecorder(result, newPath, repo, types)
|
||||||
} else return result
|
} else return result
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// The flip side: proxy that traps setting properties and invalidates things
|
// The flip side: proxy that traps setting properties and invalidates things
|
||||||
// that depend on them:
|
// 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) {
|
set(target, prop, value, receiver) {
|
||||||
// First see if this setting has any dependencies:
|
// First see if this setting has any dependencies:
|
||||||
const newPath = path.slice()
|
const newPath = [path, prop].join('.')
|
||||||
newPath.push(prop)
|
const depPerTypes = repo._dependencies.get(newPath)
|
||||||
const key = newPath.shift()
|
if (depPerTypes && Reflect.get(target, prop, receiver) !== value) {
|
||||||
const depSet = repo._dependencies[key]?.get(newPath)
|
for (const types of typesList) {
|
||||||
if (depSet?.size) {
|
repo._invalidate(depPerTypes.get(types))
|
||||||
// It does. So if we are changing it, invalidate them:
|
}
|
||||||
const oldValue = Reflect.get(target, prop, receiver)
|
|
||||||
if (value !== oldValue) repo._invalidate(depSet)
|
|
||||||
}
|
}
|
||||||
// Now we can just perform the setting
|
// Now we can just perform the setting
|
||||||
return Reflect.set(target, prop, value, receiver)
|
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
|
// Only thing we need to do is push the watching down
|
||||||
const result = Reflect.get(target, prop, receiver)
|
const result = Reflect.get(target, prop, receiver)
|
||||||
if (typeof result === 'object' && !(result instanceof Type)) {
|
if (typeof result === 'object' && !(result instanceof Type)) {
|
||||||
const newPath = path.slice()
|
const newPath = [path, prop].join('.')
|
||||||
newPath.push(prop)
|
return DependencyWatcher(result, newPath, typesList, repo)
|
||||||
return DependencyWatcher(result, newPath, repo)
|
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import {pattern} from './TypePatterns.js'
|
import {pattern, Passthru} from './TypePatterns.js'
|
||||||
|
|
||||||
export class Implementations {
|
export class Implementations {
|
||||||
constructor(imps) {
|
constructor(imps) {
|
||||||
|
@ -17,6 +17,25 @@ export class Implementations {
|
||||||
|
|
||||||
export const onType = (...imps) => new Implementations(imps)
|
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 => {
|
export const isPlainObject = obj => {
|
||||||
if (typeof obj !== 'object') return false
|
if (typeof obj !== 'object') return false
|
||||||
if (!obj) return false // excludes null
|
if (!obj) return false // excludes null
|
||||||
|
|
9
src/coretypes/README.md
Normal file
9
src/coretypes/README.md
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
## Nanomath functions on Core types
|
||||||
|
|
||||||
|
The nanomath core is quite parsimonious in terms of what it defines/requires
|
||||||
|
to be in a TypeDispatcher instance, to make the TypeDispatcher as potentially
|
||||||
|
flexible as possible for future/other applications, possibly as an independent
|
||||||
|
package.
|
||||||
|
|
||||||
|
As a result, any additional methods on the core types that we would like
|
||||||
|
to define, such as comparisons between them, are defined in this directory.
|
13
src/coretypes/__test__/relational.spec.js
Normal file
13
src/coretypes/__test__/relational.spec.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import assert from 'assert'
|
||||||
|
import math from '#nanomath'
|
||||||
|
|
||||||
|
const same = math.indistinguishable
|
||||||
|
|
||||||
|
describe('core type relational functions', () => {
|
||||||
|
it('checks if core type entities are the same', () => {
|
||||||
|
assert(same(undefined, undefined))
|
||||||
|
assert(same(math.types.Undefined, math.types.Undefined))
|
||||||
|
assert(!same(math.types.Undefined, math.types.TypeOfTypes))
|
||||||
|
assert.throws(() => same(undefined, math.Types.Undefined), TypeError)
|
||||||
|
})
|
||||||
|
})
|
1
src/coretypes/all.js
Normal file
1
src/coretypes/all.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './relational.js'
|
8
src/coretypes/relational.js
Normal file
8
src/coretypes/relational.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import {onType} from '#core/helpers.js'
|
||||||
|
import {TypeOfTypes, Undefined} from '#core/Type.js'
|
||||||
|
import {boolnum} from '#number/helpers.js'
|
||||||
|
|
||||||
|
export const indistinguishable = onType(
|
||||||
|
[Undefined, Undefined], boolnum(() => true),
|
||||||
|
[TypeOfTypes, TypeOfTypes], boolnum((t, u) => t === u)
|
||||||
|
)
|
41
src/generic/__test__/relational.spec.js
Normal file
41
src/generic/__test__/relational.spec.js
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import assert from 'assert'
|
||||||
|
import math from '#nanomath'
|
||||||
|
import * as numbers from '#number/all.js'
|
||||||
|
import * as generics from '#generic/all.js'
|
||||||
|
import {TypeDispatcher} from '#core/TypeDispatcher.js'
|
||||||
|
|
||||||
|
describe('generic relational functions', () => {
|
||||||
|
it('tests equality for anything, approx on numbers', () => {
|
||||||
|
const {equal} = math
|
||||||
|
assert(equal(undefined, undefined))
|
||||||
|
assert(equal(math.types.NumberT, math.types.NumberT))
|
||||||
|
assert(!equal(math.types.NumberT, math.types.BooleanT))
|
||||||
|
assert(!equal(undefined, math.types.NumberT))
|
||||||
|
assert(equal(1, 1))
|
||||||
|
assert(equal(true, 1)) // questionable but same as mathjs
|
||||||
|
assert(!equal(undefined, true))
|
||||||
|
assert(equal(1, 1 + 0.9e-12))
|
||||||
|
assert(equal(0, 1e-16))
|
||||||
|
assert(!equal(1, 1 + 1.1e-12))
|
||||||
|
assert(!equal(0, 1.1e-15))
|
||||||
|
})
|
||||||
|
it('adjusts equality when config changes', () => {
|
||||||
|
const jn = new TypeDispatcher(generics, numbers)
|
||||||
|
const {equal} = jn
|
||||||
|
assert.strictEqual(equal(1, 1 + 0.9e-12), 1)
|
||||||
|
assert.strictEqual(equal(0, 1e-16), 1)
|
||||||
|
assert.strictEqual(equal(1, 1 + 1.1e-12), 0)
|
||||||
|
assert.strictEqual(equal(0, 1.1e-15), 0)
|
||||||
|
|
||||||
|
jn.config.relTol = 1e-10
|
||||||
|
assert.strictEqual(equal(1, 1 + 1.1e-12), 1)
|
||||||
|
assert.strictEqual(equal(1, 1 + 1.1e-10), 0)
|
||||||
|
assert.strictEqual(equal(0, 1.1e-15), 0)
|
||||||
|
|
||||||
|
jn.config.absTol = 1e-13
|
||||||
|
assert.strictEqual(equal(1, 1 + 1.1e-12), 1)
|
||||||
|
assert.strictEqual(equal(1, 1 + 1.1e-10), 0)
|
||||||
|
assert.strictEqual(equal(0, 1.1e-15), 1)
|
||||||
|
assert.strictEqual(equal(0, 1.1e-13), 0)
|
||||||
|
})
|
||||||
|
})
|
|
@ -1 +1,3 @@
|
||||||
export * as arithmetic from './arithmetic.js'
|
export * as arithmetic from './arithmetic.js'
|
||||||
|
export * as configuration from './config.js'
|
||||||
|
export * as relational from './relational.js'
|
||||||
|
|
5
src/generic/config.js
Normal file
5
src/generic/config.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import {ImplementationsGenerator, onType} from '#core/helpers.js'
|
||||||
|
import {Passthru} from '#core/TypePatterns.js'
|
||||||
|
|
||||||
|
export const config = new ImplementationsGenerator(
|
||||||
|
() => onType(Passthru, {relTol: 1e-12, absTol: 1e-15}))
|
1
src/generic/helpers.js
Normal file
1
src/generic/helpers.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export const ReturnsAs = (g, f) => (f.returns = g.returns, f)
|
36
src/generic/relational.js
Normal file
36
src/generic/relational.js
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import {ReturnsAs} from './helpers.js'
|
||||||
|
import {onType} from '#core/helpers.js'
|
||||||
|
import {Any, matched} from '#core/TypePatterns.js'
|
||||||
|
import {boolnum} from '#number/helpers.js'
|
||||||
|
|
||||||
|
export const equal = onType(
|
||||||
|
[Any, Any], (math, [T, U]) => {
|
||||||
|
// Finding the correct signature of `indistinguishable` to use for
|
||||||
|
// testing (approximate) equality is tricky, because T or U might
|
||||||
|
// need to be converted for the sake of comparison, and some types
|
||||||
|
// allow tolerances for equality and others don't. So the plan is
|
||||||
|
// we first look up without tolerances, then we check the config for
|
||||||
|
// the matching type, and then we look up with tolerances.
|
||||||
|
let exactChecker
|
||||||
|
try {
|
||||||
|
exactChecker = math.indistinguishable.resolve([T, U])
|
||||||
|
} catch { // can't compare, so no way they can be equal
|
||||||
|
return boolnum(() => false)(math)
|
||||||
|
}
|
||||||
|
// Get the type of the first argument to the matching checker:
|
||||||
|
const ByType = matched(exactChecker.template).flat()[0]
|
||||||
|
// Now see if there are tolerances for that type:
|
||||||
|
const typeConfig = math.resolve('config', [ByType])
|
||||||
|
if ('relTol' in typeConfig) {
|
||||||
|
try {
|
||||||
|
const {relTol, absTol} = typeConfig
|
||||||
|
const RT = math.typeOf(relTol)
|
||||||
|
const AT = math.typeOf(absTol)
|
||||||
|
const approx = math.indistinguishable.resolve([T, U, RT, AT])
|
||||||
|
return ReturnsAs(
|
||||||
|
approx, (t, u) => approx(t, u, relTol, absTol))
|
||||||
|
} catch {} // fall through to case with no tolerances
|
||||||
|
}
|
||||||
|
// either no tolerances or no matching signature for indistinguishable
|
||||||
|
return exactChecker
|
||||||
|
})
|
|
@ -1,8 +1,9 @@
|
||||||
import * as booleans from './boolean/all.js'
|
import * as booleans from './boolean/all.js'
|
||||||
|
import * as coretypes from './coretypes/all.js'
|
||||||
import * as generics from './generic/all.js'
|
import * as generics from './generic/all.js'
|
||||||
import * as numbers from './number/all.js'
|
import * as numbers from './number/all.js'
|
||||||
import {TypeDispatcher} from '#core/TypeDispatcher.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
|
export default math
|
||||||
|
|
32
src/number/__test__/relational.spec.js
Normal file
32
src/number/__test__/relational.spec.js
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import assert from 'assert'
|
||||||
|
import math from '#nanomath'
|
||||||
|
|
||||||
|
const {exceeds, indistinguishable} = math
|
||||||
|
|
||||||
|
describe('number relational functions', () => {
|
||||||
|
it('orders numbers correctly', () => {
|
||||||
|
assert(exceeds(7, 2.2))
|
||||||
|
assert(exceeds(0, -1.1))
|
||||||
|
assert(exceeds(1e-35, 0))
|
||||||
|
assert(exceeds(Infinity, 1e99))
|
||||||
|
assert(exceeds(-1e101, -Infinity))
|
||||||
|
assert(!exceeds(NaN, 0))
|
||||||
|
assert(!exceeds(0, NaN))
|
||||||
|
assert(!exceeds(2, 2))
|
||||||
|
})
|
||||||
|
it('checks for exact equality', () => {
|
||||||
|
assert(indistinguishable(0, 0))
|
||||||
|
assert(indistinguishable(0, -0))
|
||||||
|
assert(!indistinguishable(0, 1e-35))
|
||||||
|
assert(!indistinguishable(NaN, NaN))
|
||||||
|
assert(indistinguishable(Infinity, Infinity))
|
||||||
|
})
|
||||||
|
it('checks for approximate equality', () => {
|
||||||
|
const rel = 1e-12
|
||||||
|
const abs = 1e-15
|
||||||
|
assert(indistinguishable(1, 1 + 0.9e-12, rel, abs))
|
||||||
|
assert(indistinguishable(0, 1e-16, rel, abs))
|
||||||
|
assert(!indistinguishable(1, 1 + 1.1e-12, rel, abs))
|
||||||
|
assert(!indistinguishable(0, 1e-14, rel, abs))
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,4 +1,5 @@
|
||||||
export * as typeDefinition from './NumberT.js'
|
export * as typeDefinition from './NumberT.js'
|
||||||
export * as arithmetic from './arithmetic.js'
|
export * as arithmetic from './arithmetic.js'
|
||||||
|
export * as relational from './relational.js'
|
||||||
export * as type from './type.js'
|
export * as type from './type.js'
|
||||||
export * as utils from './utils.js'
|
export * as utils from './utils.js'
|
||||||
|
|
|
@ -6,4 +6,11 @@ import {Returns} from '#core/Type.js'
|
||||||
export const plain = f => onType(
|
export const plain = f => onType(
|
||||||
Array(f.length).fill(NumberT), Returns(NumberT, f))
|
Array(f.length).fill(NumberT), Returns(NumberT, f))
|
||||||
|
|
||||||
export const boolnum = Returns(NumberT, p => p ? 1 : 0)
|
// Takes a behavior returning boolean, and returns a factory
|
||||||
|
// that returns that behavior if the boolean type is present,
|
||||||
|
// and otherwise wraps the behavior to return 1 or 0.
|
||||||
|
export const boolnum = behavior => math => {
|
||||||
|
const {BooleanT} = math.types
|
||||||
|
if (BooleanT) return Returns(BooleanT, behavior)
|
||||||
|
return Returns(NumberT, (...args) => behavior(...args) ? 1 : 0)
|
||||||
|
}
|
||||||
|
|
37
src/number/relational.js
Normal file
37
src/number/relational.js
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import {onType} from '#core/helpers.js'
|
||||||
|
import {Returns} from '#core/Type.js'
|
||||||
|
import {Optional} from '#core/TypePatterns.js'
|
||||||
|
import {boolnum} from './helpers.js'
|
||||||
|
import {NumberT} from './NumberT.js'
|
||||||
|
|
||||||
|
// In nanomath, we take the point of view that two comparators are primitive:
|
||||||
|
// indistinguishable(a, b, relTol, absTol), and exceeds(a, b). All others
|
||||||
|
// are defined generically in terms of these. They typically return BooleanT,
|
||||||
|
// but in a numbers-only bundle, they return 1 or 0.
|
||||||
|
|
||||||
|
// Notice a feature of TypedDispatcher: if you specify one tolerance, you must
|
||||||
|
// specify both.
|
||||||
|
export const indistinguishable = onType(
|
||||||
|
[NumberT, NumberT, Optional([NumberT, NumberT])],
|
||||||
|
boolnum((a, b, [tolerances = [0, 0]]) => {
|
||||||
|
const [relTol, absTol] = tolerances
|
||||||
|
if (relTol < 0 || absTol < 0) {
|
||||||
|
throw new RangeError(
|
||||||
|
`Tolerances (relative: ${relTol}, absolute: ${absTol}) `
|
||||||
|
+ 'must be nonnegative')
|
||||||
|
}
|
||||||
|
if (isNaN(a) || isNaN(b)) return false
|
||||||
|
if (a === b) return true
|
||||||
|
if (!isFinite(a) || !isFinite(b)) return false
|
||||||
|
// |a-b| <= absTol or |a-b| <= relTol*max(|a|, |b|)
|
||||||
|
const diff = Math.abs(a-b)
|
||||||
|
if (diff <= absTol) return true
|
||||||
|
const magnitude = Math.max(Math.abs(a), Math.abs(b))
|
||||||
|
return diff <= relTol * magnitude
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// Returns truthy if a (interpreted as completely precise) represents a
|
||||||
|
// greater value than b (interpreted as completely precise). Note that even if
|
||||||
|
// so, a and b might be indistinguishable() to some tolerances.
|
||||||
|
export const exceeds = onType([NumberT, NumberT], boolnum((a, b) => a > b))
|
|
@ -1,4 +1,4 @@
|
||||||
import {plain, boolnum} from './helpers.js'
|
import {plain} from './helpers.js'
|
||||||
import {BooleanT} from '#boolean/BooleanT.js'
|
import {BooleanT} from '#boolean/BooleanT.js'
|
||||||
import {Returns} from '#core/Type.js'
|
import {Returns} from '#core/Type.js'
|
||||||
import {NumberT} from '#number/NumberT.js'
|
import {NumberT} from '#number/NumberT.js'
|
||||||
|
@ -7,6 +7,6 @@ const num = f => Returns(NumberT, f)
|
||||||
|
|
||||||
export const number = plain(a => a)
|
export const number = plain(a => a)
|
||||||
number.also(
|
number.also(
|
||||||
BooleanT, boolnum,
|
BooleanT, num(p => p ? 1 : 0),
|
||||||
[], num(() => 0)
|
[], num(() => 0)
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,8 +5,4 @@ import {Returns} from '#core/Type.js'
|
||||||
import {onType} from '#core/helpers.js'
|
import {onType} from '#core/helpers.js'
|
||||||
|
|
||||||
export const clone = plain(a => a)
|
export const clone = plain(a => a)
|
||||||
export const isnan = onType(NumberT, math => {
|
export const isnan = onType(NumberT, boolnum(isNaN))
|
||||||
const {BooleanT} = math.types
|
|
||||||
if (BooleanT) return Returns(BooleanT, a => isNaN(a))
|
|
||||||
return Returns(NumberT, a => boolnum(isNaN(a)))
|
|
||||||
})
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue