feat: config and approximate equality (#19)
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:
Glen Whitney 2025-04-16 04:23:48 +00:00 committed by Glen Whitney
parent 27fa4b0193
commit 70ce01d12b
27 changed files with 788 additions and 218 deletions

48
src/core/README.md Normal file
View 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.

View file

@ -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)

View file

@ -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
}

View file

@ -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)
})
})

View file

@ -1,4 +1,4 @@
import {pattern} from './TypePatterns.js'
import {pattern, Passthru} from './TypePatterns.js'
export class Implementations {
constructor(imps) {
@ -17,6 +17,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