feat: config and approximate equality (#19)
All checks were successful
/ test (push) Successful in 17s
All checks were successful
/ test (push) 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. Reviewed-on: #19 Co-authored-by: Glen Whitney <glen@studioinfinity.org> Co-committed-by: Glen Whitney <glen@studioinfinity.org>
This commit is contained in:
parent
27fa4b0193
commit
70ce01d12b
27 changed files with 788 additions and 218 deletions
48
src/core/README.md
Normal file
48
src/core/README.md
Normal file
|
@ -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.
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue