diff --git a/src/boolean/__test__/type.spec.js b/src/boolean/__test__/type.spec.js index ad294a7..2a8c355 100644 --- a/src/boolean/__test__/type.spec.js +++ b/src/boolean/__test__/type.spec.js @@ -19,7 +19,7 @@ describe('boolean type functions', () => { }) it('converts any type to boolean', () => { for (const T in math.types) { - if (T instanceof Type) assert(boolean.resolve([T])) + if (Type.holds(T)) assert(boolean.resolve([T])) } }) }) diff --git a/src/core/Implementations.js b/src/core/Implementations.js index c53ea3e..62e3516 100644 --- a/src/core/Implementations.js +++ b/src/core/Implementations.js @@ -1,13 +1,23 @@ +const implementationsBrand = Symbol() export class Implementations { constructor(impOrImps) { if (Array.isArray(impOrImps)) { this.matchers = impOrImps } else this.matchers = [impOrImps] + this[implementationsBrand] = true } + + // Returns true if entity is an Implementations + static holds(entity) {return entity[implementationsBrand]} } +const igBrand = Symbol() export class ImplementationsGenerator { constructor(f) { this.generate = f + this[igBrand] = true } + + // Returns true if entity is an ImplementationsGenerator + static holds(entity) {return entity[igBrand]} } diff --git a/src/core/Type.js b/src/core/Type.js index 7bb7e56..42a4f3d 100644 --- a/src/core/Type.js +++ b/src/core/Type.js @@ -1,5 +1,7 @@ import ArrayKeyedMap from 'array-keyed-map' +const typeBrand = Symbol() // invisible outside this file + // Generic types are callable, so we have no choice but to extend Function export class Type extends Function { constructor(f, options = {}) { @@ -19,6 +21,7 @@ export class Type extends Function { } }) + this[typeBrand] = true this.test = f // we want property `from` to end up as an array of Matchers: this.from = options.from @@ -57,15 +60,16 @@ export class Type extends Function { return rewired } - toString() { - return this.typeName || `[Type ${this.test}]` - } + toString() {return this.typeName || `[Type ${this.test}]`} + + // Returns true if entity is a Type + static holds(entity) {return entity[typeBrand]} } 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 TypeOfTypes = new Type(t => Type.holds(t)) export const Unknown = new Type(() => true) // Danger, do not merge! Unknown._doNotMerge = true @@ -74,7 +78,7 @@ export const OneOf = (...types) => { if (!types.length) { throw new RangeError('cannot choose OneOf no types at all') } - const nonType = types.findIndex(T => !(T instanceof Type)) + const nonType = types.findIndex(T => !Type.holds(T)) if (nonType >= 0) { throw new RangeError( `OneOf can only take type arguments, not ${types[nonType]}`) @@ -106,7 +110,7 @@ export const ReturnType = f => f.returns ?? Unknown export const whichType = typs => Returns(TypeOfTypes, item => { for (const type of Object.values(typs)) { - if (!(type instanceof Type)) continue + if (!Type.holds(type)) continue if (type.test(item)) return type.refine(item, whichType(typs)) } let errorMsg = '' diff --git a/src/core/TypeDispatcher.js b/src/core/TypeDispatcher.js index 5c96fb1..cca4932 100644 --- a/src/core/TypeDispatcher.js +++ b/src/core/TypeDispatcher.js @@ -81,10 +81,10 @@ export class TypeDispatcher { // 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() + if (ImplementationsGenerator.holds(val)) val = val.generate() // Now dispatch on what sort of thing we are supposed to merge: - if (val instanceof Type) { + if (Type.holds(val)) { if (val._doNotMerge) { throw new TypeError(`attempt to merge unusable type '${val}'`) } @@ -105,13 +105,13 @@ export class TypeDispatcher { // Everything else we coerce into Implementations and deal with // right here: - if (val instanceof Matcher) val = new Implementations(val) + if (Matcher.holds(val)) val = new Implementations(val) if (Array.isArray(val)) val = new Implementations(val) if (isPlainFunction(val)) { throw new RangeError( `function value for ${key} must be merged within a 'match' call`) } - if (!(val instanceof Implementations)) { + if (!Implementations.holds(val)) { val = new Implementations(match(Passthru, val)) } @@ -307,7 +307,7 @@ export class TypeDispatcher { } if (generatingDeps && typeof result === 'object' - && !(result instanceof Type) + && !Type.holds(result) ) { return DependencyRecorder(result, key, this, bhvix) } @@ -354,7 +354,7 @@ export class TypeDispatcher { behave.set(bhvix, item) if (generatingDeps && typeof item === 'object' - && !(item instanceof Type) + && !Type.holds(item) ) { return DependencyRecorder(item, key, this, bhvix) } @@ -469,7 +469,8 @@ const DependencyRecorder = (object, path, repo, bhvix) => new Proxy(object, { const result = Reflect.get(target, prop, receiver) // pass internal methods through, as well as resolve calls, // since we record dependencies within the latter: - if (prop.startsWith('_') + if (typeof prop === 'symbol' + || prop.startsWith('_') || prop === 'resolve' || (typeof result === 'function' && 'isDispatcher' in result) ) { @@ -484,7 +485,7 @@ const DependencyRecorder = (object, path, repo, bhvix) => new Proxy(object, { // 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)) { + if (typeof result === 'object' && !Type.holds(result)) { return DependencyRecorder(result, newPath, repo, bhvix) } else return result } @@ -508,7 +509,7 @@ const DependencyWatcher = (object, path, ixList, repo) => new Proxy(object, { get(target, prop, receiver) { // 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)) { + if (typeof result === 'object' && !Type.holds(result)) { const newPath = [path, prop].join('.') return DependencyWatcher(result, newPath, ixList, repo) } diff --git a/src/core/TypePatterns.js b/src/core/TypePatterns.js index 3fe6d4a..cf3d58a 100644 --- a/src/core/TypePatterns.js +++ b/src/core/TypePatterns.js @@ -1,7 +1,12 @@ import {ReturnType, Type, Undefined, Unknown} from './Type.js' import {isPlainFunction} from './helpers.js' +const tpBrand = Symbol() + export class TypePattern { + constructor() { + this[tpBrand] = true + } match(_typeSequence, _options={}) { throw new Error('Specific TypePatterns must implement match') } @@ -10,6 +15,9 @@ export class TypePattern { } equal(other) {return other.constructor === this.constructor} toString() {return 'Abstract Pattern (?!)'} + + // Returns true if entity is a TypePattern + static holds(entity) {return entity[tpBrand]} } class MatchTypePattern extends TypePattern { @@ -91,8 +99,8 @@ class PredicatePattern extends TypePattern { } export const pattern = patternOrSpec => { - if (patternOrSpec instanceof TypePattern) return patternOrSpec - if (patternOrSpec instanceof Type) { + if (TypePattern.holds(patternOrSpec)) return patternOrSpec + if (Type.holds(patternOrSpec)) { return new MatchTypePattern(patternOrSpec) } if (Array.isArray(patternOrSpec)) return new SequencePattern(patternOrSpec) @@ -206,11 +214,16 @@ export const needsCollection = (template) => { return 'actual' in template } +const matcherBrand = Symbol() export class Matcher { constructor(spec, facOrBehave) { this.pattern = pattern(spec) this.does = facOrBehave + this[matcherBrand] = true } + + // Returns true if entity is a matcher + static holds(entity) {return entity[matcherBrand]} } export const match = (spec, facOrBehave) => new Matcher(spec, facOrBehave) diff --git a/src/core/__test__/helpers.spec.js b/src/core/__test__/helpers.spec.js index 46576eb..10006b2 100644 --- a/src/core/__test__/helpers.spec.js +++ b/src/core/__test__/helpers.spec.js @@ -7,8 +7,8 @@ import {match, Matcher, TypePattern} from '../TypePatterns.js' describe('Core helpers', () => { it('defines what Matchers are', () => { const matcher = match([TypeOfTypes, Undefined], -3) - assert(matcher instanceof Matcher) - assert(matcher.pattern instanceof TypePattern) + assert(Matcher.holds(matcher)) + assert(TypePattern.holds(matcher.pattern)) }) it('detects plain objects', () => {