From 27fa4b0193ddb83e8696283a7c2305d671c1f636 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sun, 13 Apr 2025 16:29:51 +0000 Subject: [PATCH] feat: Introduce BooleanT and boolean functions (#17) This PR adds a boolean section, as well as an isNaN predicate on numbers. In a TypeDispatcher, when BooleanT is present, isNaN returns a BooleanT. However, in a numbers-only TypeDispatcher, it returns 1 or 0 instead. Moreover, when booleans are subsequently added to a numbers-only instance, isNaN properly reconfigures itself to return BooleanT. No predicates that depend on approximate equality testing or a configuration object are implemented in this PR. This PR also implements type matching and dispatching with implicit conversions, and adds an implicit conversion from BooleanT to NumberT. Reviewed-on: https://code.studioinfinity.org/StudioInfinity/nanomath/pulls/17 Co-authored-by: Glen Whitney Co-committed-by: Glen Whitney --- package.json5 | 3 + pnpm-lock.yaml | 9 ++ src/__test__/numbers.spec.js | 13 ++ src/boolean/BooleanT.js | 3 + src/boolean/__test__/BooleanT.spec.js | 24 +++ src/boolean/__test__/type.spec.js | 25 +++ src/boolean/all.js | 2 + src/boolean/type.js | 14 ++ src/core/Type.js | 18 ++- src/core/TypeDispatcher.js | 193 +++++++++++++++-------- src/core/TypePatterns.js | 88 ++++++++--- src/core/__test__/Type.spec.js | 10 +- src/core/__test__/TypeDispatcher.spec.js | 38 +++-- src/core/__test__/TypePatterns.spec.js | 10 +- src/core/__test__/helpers.spec.js | 4 +- src/core/helpers.js | 11 +- src/generic/__test__/arithmetic.spec.js | 4 +- src/generics.js | 1 - src/nanomath.js | 7 +- src/number/Number.js | 3 - src/number/NumberT.js | 7 + src/number/__test__/Number.spec.js | 11 -- src/number/__test__/NumberT.spec.js | 26 +++ src/number/__test__/type.spec.js | 4 + src/number/__test__/utils.spec.js | 5 + src/number/all.js | 2 +- src/number/helpers.js | 6 +- src/number/type.js | 12 +- src/number/utils.js | 11 +- src/numbers.js | 8 +- src/package.json | 2 + 31 files changed, 432 insertions(+), 142 deletions(-) create mode 100644 src/__test__/numbers.spec.js create mode 100644 src/boolean/BooleanT.js create mode 100644 src/boolean/__test__/BooleanT.spec.js create mode 100644 src/boolean/__test__/type.spec.js create mode 100644 src/boolean/all.js create mode 100644 src/boolean/type.js delete mode 100644 src/generics.js delete mode 100644 src/number/Number.js create mode 100644 src/number/NumberT.js delete mode 100644 src/number/__test__/Number.spec.js create mode 100644 src/number/__test__/NumberT.spec.js diff --git a/package.json5 b/package.json5 index 19d0d4f..50e6463 100644 --- a/package.json5 +++ b/package.json5 @@ -18,4 +18,7 @@ devDependencies: { mocha: '^11.1.0', }, + dependencies: { + 'array-keyed-map': '^2.1.3', + }, } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3a24d24..2704e5b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,10 @@ settings: importers: .: + dependencies: + array-keyed-map: + specifier: ^2.1.3 + version: 2.1.3 devDependencies: mocha: specifier: ^11.1.0 @@ -49,6 +53,9 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + array-keyed-map@2.1.3: + resolution: {integrity: sha512-JIUwuFakO+jHjxyp4YgSiKXSZeC0U+R1jR94bXWBcVlFRBycqXlb+kH9JHxBGcxnVuSqx5bnn0Qz9xtSeKOjiA==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -398,6 +405,8 @@ snapshots: argparse@2.0.1: {} + array-keyed-map@2.1.3: {} + balanced-match@1.0.2: {} binary-extensions@2.3.0: {} diff --git a/src/__test__/numbers.spec.js b/src/__test__/numbers.spec.js new file mode 100644 index 0000000..95f3658 --- /dev/null +++ b/src/__test__/numbers.spec.js @@ -0,0 +1,13 @@ +import assert from 'assert' +import math from '../numbers.js' + +describe('the numbers-only bundle', () => { + it('works like regular on number-only functions', () => { + assert.strictEqual(math.quotient(5, 3), 1) + assert.strictEqual(math.square(-3), 9) + }) + it('uses 1 and 0 instead of true and false', () => { + assert.strictEqual(math.isnan(-16.5), 0) + assert.strictEqual(math.isnan(NaN), 1) + }) +}) diff --git a/src/boolean/BooleanT.js b/src/boolean/BooleanT.js new file mode 100644 index 0000000..8d26974 --- /dev/null +++ b/src/boolean/BooleanT.js @@ -0,0 +1,3 @@ +import {Type} from '#core/Type.js' + +export const BooleanT = new Type(n => typeof n === 'boolean') diff --git a/src/boolean/__test__/BooleanT.spec.js b/src/boolean/__test__/BooleanT.spec.js new file mode 100644 index 0000000..748a2e8 --- /dev/null +++ b/src/boolean/__test__/BooleanT.spec.js @@ -0,0 +1,24 @@ +import assert from 'assert' +import {BooleanT} from '../BooleanT.js' +import math from '#nanomath' + +describe('BooleanT Type', () => { + it('correctly recognizes booleans', () => { + assert(BooleanT.test(true)) + assert(BooleanT.test(false)) + assert(!BooleanT.test(null)) + assert(!BooleanT.test(1)) + }) + it('autoconverts to number type', () => { + assert.strictEqual(math.abs(false), 0) + assert.strictEqual(math.absquare(true), 1) + assert.strictEqual(math.add(true, true), 2) + assert.strictEqual(math.divide(false, true), 0) + assert.strictEqual(math.cbrt(true), 1) + assert.strictEqual(math.invert(true), 1) + assert.strictEqual(math.multiply(false, false), 0) + assert.strictEqual(math.negate(false), -0) + assert.strictEqual(math.subtract(false, true), -1) + assert.strictEqual(math.quotient(true, true), 1) + }) +}) diff --git a/src/boolean/__test__/type.spec.js b/src/boolean/__test__/type.spec.js new file mode 100644 index 0000000..ad294a7 --- /dev/null +++ b/src/boolean/__test__/type.spec.js @@ -0,0 +1,25 @@ +import assert from 'assert' +import math from '#nanomath' +import {Type} from '#core/Type.js' + +const boolean = math.boolean + +describe('boolean type functions', () => { + it('properly converts to boolean', () => { + assert.strictEqual(boolean(false), false) + assert.strictEqual(boolean(true), true) + assert.strictEqual(boolean(0), false) + assert.strictEqual(boolean(-0), false) + assert.strictEqual(boolean(NaN), false) + assert.strictEqual(boolean(Infinity), true) + assert.strictEqual(boolean(1e-30), true) + assert.strictEqual(boolean(undefined), false) + assert.strictEqual(boolean(), false) + assert.strictEqual(boolean(math.types.NumberT), true) + }) + it('converts any type to boolean', () => { + for (const T in math.types) { + if (T instanceof Type) assert(boolean.resolve([T])) + } + }) +}) diff --git a/src/boolean/all.js b/src/boolean/all.js new file mode 100644 index 0000000..443a8fa --- /dev/null +++ b/src/boolean/all.js @@ -0,0 +1,2 @@ +export * as typeDefinition from './BooleanT.js' +export * as type from './type.js' diff --git a/src/boolean/type.js b/src/boolean/type.js new file mode 100644 index 0000000..169a207 --- /dev/null +++ b/src/boolean/type.js @@ -0,0 +1,14 @@ +import {BooleanT} from './BooleanT.js' +import {onType} from '#core/helpers.js' +import {Returns, Type, TypeOfTypes, Undefined} from '#core/Type.js' +import {NumberT} from '#number/NumberT.js' + +const bool = f => Returns(BooleanT, f) + +export const boolean = onType( + BooleanT, bool(p => p), + NumberT, bool(a => !!a), + TypeOfTypes, bool(() => true), + Undefined, bool(() => false), + [], bool(() => false) +) diff --git a/src/core/Type.js b/src/core/Type.js index cb3f8b0..7c728a2 100644 --- a/src/core/Type.js +++ b/src/core/Type.js @@ -1,10 +1,10 @@ -const typeObject = {} // have to make sure there is only one - -export const types = () => typeObject - export class Type { - constructor(f) { + constructor(f, options = {}) { this.test = f + this.from = options.from ?? {patterns: []} // mock empty Implementations + } + toString() { + return this.name || `[Type ${this.test}]` } } @@ -13,8 +13,8 @@ export const TypeOfTypes = new Type(t => t instanceof Type) export const Returns = (type, f) => (f.returns = type, f) -export const typeOf = Returns(TypeOfTypes, item => { - for (const type of Object.values(typeObject)) { +export const whichType = typs => Returns(TypeOfTypes, item => { + for (const type of Object.values(typs)) { if (!(type instanceof Type)) continue if (type.test(item)) return type } @@ -27,10 +27,12 @@ export const typeOf = Returns(TypeOfTypes, item => { throw new TypeError(errorMsg) }) +export const typeOf = math => whichType(math.types) + // bootstrapping order matters, but order of exports in a module isn't // simply the order that the items are listed in the module. So we make // an explicitly ordered export of implementations for this sake: export const bootstrapTypes = { - types, Type, Undefined, TypeOfTypes, typeOf + Type, Undefined, TypeOfTypes, typeOf } diff --git a/src/core/TypeDispatcher.js b/src/core/TypeDispatcher.js index 23e1d54..64757ce 100644 --- a/src/core/TypeDispatcher.js +++ b/src/core/TypeDispatcher.js @@ -1,27 +1,20 @@ -import {typeOf, Type, bootstrapTypes} from './Type.js' +import ArrayKeyedMap from 'array-keyed-map' import { Implementations, isPlainFunction, isPlainObject, onType } from './helpers.js' -import {alwaysMatches, Any, Multiple} from './TypePatterns.js' - -// helper that organizes a list into the same chunks as a pattern is -const collectLike = (list, pattern, state = {pos: 0}) => { - const result = [] - for (const elt of pattern) { - if (Array.isArray(elt)) result.push(collectLike(list, elt, state)) - else result.push(list[state.pos++]) - } - return result -} +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._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 for (const spec of specs) this.merge(spec) } @@ -39,37 +32,57 @@ export class TypeDispatcher { for (const key in spec) { let val = spec[key] if (val instanceof Type) { - // TODO: Need to wipe out any dependencies on types[key]! + // 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 + val.name = key continue } if (typeof val === 'function') { - val = onType(Multiple(Any), val) + val = onType(Passthru, val) } if (val instanceof Implementations) { if (!(key in this)) { - // need to set up the item + // 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 - let tryTypes = [] try { tryValue = this.resolve(key, []) } catch { - // Has no value for the empty type list, so - // find a type list it will have a value for - tryTypes = - this._implementations[key][0][0].sampleTypes() - tryValue = this.resolve(key, tryTypes) + // 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(typeOf) + const types = args.map(thisTypeOf) return this.resolve(key, types)(...args) } standard.resolve = (types) => this.resolve(key, types) @@ -81,7 +94,7 @@ export class TypeDispatcher { }) return standard } - if (tryTypes.length) tryValue = undefined + if (typeof tryValue === 'object') { if (!('resolve' in tryValue)) { tryValue.resolve = types => this.resolve(key, types) @@ -99,6 +112,7 @@ export class TypeDispatcher { }) return get() } + Object.defineProperty(this, key, { enumerable: true, configurable: true, @@ -107,13 +121,16 @@ export class TypeDispatcher { return tryValue } }) + + // Finally, initialize the other data for this key: this._implementations[key] = [] - this._behaviors[key] = new Map() - this._dependencies[key] = new Map() + 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 (alwaysMatches(pattern)) { + if (pattern === Passthru) { if (key in this._fallbacks) this._disengageFallback(key) this._fallbacks[key] = result } else { @@ -135,7 +152,7 @@ export class TypeDispatcher { } // install value as a catchall value - this.merge({[key]: onType(Multiple(Any), val)}) + this.merge({[key]: onType(Passthru, val)}) } } @@ -146,6 +163,29 @@ export class TypeDispatcher { for (const pair of this.resolve._genDepsOf) depSet.add(pair) } + // 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]) + } + } + return args => extractors.map(f => f(args)) + } + resolve(key, types) { if (!(key in this)) { throw new ReferenceError(`no method or value for key '${key}'`) @@ -154,64 +194,84 @@ export class TypeDispatcher { if (this.resolve._genDepsOf?.length) 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) + + // Otherwise, perform the resolution and cache the result const imps = this._implementations[key] let needItem = true let item - let pattern let template if (imps.length) { - for ([pattern, item] of imps) { - let finalIndex - ;[finalIndex, template] = pattern.match(types) - if (finalIndex === types.length) { - needItem = false - break + for (const options of [{}, {convert: true}]) { + let pattern + for ([pattern, item] of imps) { + let finalIndex + ;[finalIndex, template] = pattern.match(types, options) + if (finalIndex === types.length) { + needItem = false + break + } } + if (!needItem) break } } if (needItem && key in this._fallbacks) { needItem = false item = this._fallbacks[key] - template = [types] + template = types } if (needItem) { throw new TypeError(`no matching definition of '${key}' on '${types}'`) } - if (!isPlainFunction(item) || 'returns' in item) { + // If this key is producing a non-function value, we're done + if (!isPlainFunction(item)) { behave.set(types, item) return item } - // item is a Factory. We have to use it to build the behavior + + // item is a function, either a direct behavior or + // a factory. We have to use it to build the final behavior // First set up to record dependencies if (!('_genDepsOf' in this.resolve)) { this.resolve._genDepsOf = [] } - this.resolve._genDepsOf.push([key, types]) + let theBehavior = () => undefined - try { - theBehavior = item(DependencyRecorder(this, [], this), template) - } catch { - behave.set(types, item) - return item - } finally { - this.resolve._genDepsOf.pop() - } - if (typeof theBehavior === 'function' - && theBehavior.length - && Array.isArray(template) - && template.some(elt => Array.isArray(elt)) - ) { - // have to wrap the behavior to collect the actual arguments - // in the way corresponding to the template - const wrappedBehavior = (...args) => { - const collectedArgs = collectLike(args, template) - return theBehavior(...collectedArgs) + 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 = 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))) } - theBehavior = wrappedBehavior } - behave.set(types, theBehavior) - return theBehavior + + this.resolve._genDepsOf.pop() // OK, now it's safe to return + behave.set(types, finalBehavior) + return finalBehavior } // Method called to invalidate a set of behaviors @@ -267,7 +327,11 @@ const DependencyRecorder = (object, path, repo) => 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' || 'isDispatcher' in result) return result + if (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: @@ -278,8 +342,9 @@ const DependencyRecorder = (object, path, repo) => new Proxy(object, { repo._addToDeps(key, subpath) // 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: - if (typeof result === 'object') { + // 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) } else return result } @@ -292,7 +357,7 @@ const DependencyWatcher = (object, path, repo) => new Proxy(object, { // First see if this setting has any dependencies: const newPath = path.slice() newPath.push(prop) - const key = newPath.unshift() + const key = newPath.shift() const depSet = repo._dependencies[key]?.get(newPath) if (depSet?.size) { // It does. So if we are changing it, invalidate them: @@ -305,7 +370,7 @@ const DependencyWatcher = (object, path, 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') { + if (typeof result === 'object' && !(result instanceof Type)) { const newPath = path.slice() newPath.push(prop) return DependencyWatcher(result, newPath, repo) diff --git a/src/core/TypePatterns.js b/src/core/TypePatterns.js index 3db4f03..efe05f8 100644 --- a/src/core/TypePatterns.js +++ b/src/core/TypePatterns.js @@ -1,7 +1,7 @@ import {Type, Undefined} from './Type.js' export class TypePattern { - match(typeSequence, position = 0) { + match(typeSequence, options={}) { throw new Error('Specific TypePatterns must implement match') } sampleTypes() { @@ -15,11 +15,19 @@ class MatchTypePattern extends TypePattern { super() this.type = typeToMatch } - match(typeSequence, position = 0) { - if (position < typeSequence.length - && typeSequence[position] === this.type - ) { - return [position + 1, typeSequence[position]] + match(typeSequence, options={}) { + const position = options.position ?? 0 + const actual = typeSequence[position] + if (position < typeSequence.length) { + if (actual === this.type) return [position + 1, actual] + if (options.convert) { + for (const [pattern, convertor] of this.type.from.patterns) { + const [pos] = pattern.match([actual]) + if (pos === 1) { + return [position + 1, {actual, convertor, matched: this.type}] + } + } + } } return [-1, Undefined] } @@ -32,15 +40,19 @@ class SequencePattern extends TypePattern { super() this.patterns = itemsToMatch.map(pattern) } - match(typeSequence, position = 0) { + match(typeSequence, options={_internal: true}) { + options = options._internal + ? options + : Object.assign({_internal: true}, options) + options.position ??= 0 const matches = [] for (const pat of this.patterns) { - const [newPos, newMatch] = pat.match(typeSequence, position) + const [newPos, newMatch] = pat.match(typeSequence, options) if (newPos < 0) return [-1, Undefined] - position = newPos + options.position = newPos matches.push(newMatch) } - return [position, matches] + return [options.position, matches] } sampleTypes() { return this.patterns.map(pat => pat.sampleTypes()).flat() @@ -62,7 +74,8 @@ export const pattern = patternOrSpec => { } class AnyPattern extends TypePattern { - match(typeSequence, position = 0) { + match(typeSequence, options={}) { + const position = options.position ?? 0 return position < typeSequence.length ? [position + 1, typeSequence[position]] : [-1, Undefined] @@ -77,14 +90,18 @@ class OptionalPattern extends TypePattern { super() this.pattern = pattern(item) } - match(typeSequence, position = 0) { + match(typeSequence, options={_internal: true}) { + options = options._internal + ? options + : Object.assign({_internal: true}, options) + options.position ??= 0 const matches = [] - const [newPos, newMatch] = this.pattern.match(typeSequence, position) + const [newPos, newMatch] = this.pattern.match(typeSequence, options) if (newPos >= 0) { - position = newPos + options.position = newPos matches.push(newMatch) } - return [position, matches] + return [options.position, matches] } sampleTypes() {return []} equal(other) { @@ -99,12 +116,16 @@ class MultiPattern extends TypePattern { super() this.pattern = pattern(item) } - match(typeSequence, position = 0) { + match(typeSequence, options={_internal: true}) { + options = options._internal + ? options + : Object.assign({_internal: true}, options) + options.position ??= 0 const matches = [] while (true) { - const [newPos, newMatch] = this.pattern.match(typeSequence, position) - if (newPos < 0) return [position, matches] - position = newPos + const [newPos, newMatch] = this.pattern.match(typeSequence, options) + if (newPos < 0) return [options.position, matches] + options.position = newPos matches.push(newMatch) } } @@ -115,5 +136,30 @@ class MultiPattern extends TypePattern { } export const Multiple = item => new MultiPattern(item) -export const alwaysMatches = - pat => pat instanceof MultiPattern && pat.pattern instanceof AnyPattern + +// Like Multiple(Any) except leaves the argument list alone; it doesn't +// chunk it into a single Array of all arguments +class PassthruPattern extends TypePattern { + match(typeSequence, options={}) { + const position = options.position ?? 0 + return [typeSequence.length, typeSequence.slice(position)] + } + sampleTypes() {return []} +} + +export const Passthru = new PassthruPattern() + +// returns the template just of matched types, dropping any actual types +export const matched = (template) => { + if (Array.isArray(template)) return template.map(matched) + return template.matched ?? template +} + +// checks if the template is just pass-through or needs collection +export const needsCollection = (template) => { + if (Array.isArray(template)) { + return template.some( + elt => Array.isArray(elt) || needsCollection(elt)) + } + return 'actual' in template +} diff --git a/src/core/__test__/Type.spec.js b/src/core/__test__/Type.spec.js index 7563672..84efb23 100644 --- a/src/core/__test__/Type.spec.js +++ b/src/core/__test__/Type.spec.js @@ -1,14 +1,14 @@ import assert from 'assert' import math from '#nanomath' -import {Number} from '#number/Number.js' +import {NumberT} from '#number/NumberT.js' import {Returns} from '../Type.js' import {isPlainFunction} from '../helpers.js' describe('Core types', () => { it('creates an object with all of the types', () => { - assert('Number' in math.types) - assert.strictEqual(math.types.Number, Number) + assert('NumberT' in math.types) + assert.strictEqual(math.types.NumberT, NumberT) assert('Undefined' in math.types) assert(!('Type' in math.types)) assert(math.types.Undefined.test(undefined)) @@ -18,9 +18,9 @@ describe('Core types', () => { it('supports a typeOf operator', () => { const tO = math.typeOf - assert.strictEqual(tO(7), Number) + assert.strictEqual(tO(7), NumberT) assert.strictEqual(tO(undefined), math.types.Undefined) - assert.strictEqual(tO(Number), math.types.TypeOfTypes) + assert.strictEqual(tO(NumberT), math.types.TypeOfTypes) assert.throws(() => tO(Symbol()), TypeError) }) diff --git a/src/core/__test__/TypeDispatcher.spec.js b/src/core/__test__/TypeDispatcher.spec.js index 009b8ef..81d91a3 100644 --- a/src/core/__test__/TypeDispatcher.spec.js +++ b/src/core/__test__/TypeDispatcher.spec.js @@ -1,7 +1,8 @@ import assert from 'assert' import {TypeDispatcher} from '../TypeDispatcher.js' -import {numbers} from '../../numbers.js' -import {generics} from '../../generics.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 {Any} from "#core/TypePatterns.js" import {Returns} from "#core/Type.js" @@ -13,8 +14,8 @@ describe('TypeDispatcher', () => { assert.strictEqual( incremental.typeOf(undefined), incremental.types.Undefined) incremental.merge(numbers) - const {Number, TypeOfTypes, Undefined} = incremental.types - assert(Number.test(7)) + const {NumberT, TypeOfTypes, Undefined} = incremental.types + assert(NumberT.test(7)) assert.strictEqual(incremental.add(-1.5, 0.5), -1) // Make Undefined act like zero: incremental.merge({add: onType( @@ -27,18 +28,22 @@ describe('TypeDispatcher', () => { TypeOfTypes) assert.strictEqual(incremental.add(undefined, -3.25), -3.25) assert.strictEqual( - incremental.add.resolve([Undefined, Number]).returns, - Number) + incremental.add.resolve([Undefined, NumberT]).returns, + NumberT) // Oops, changed my mind, make it work like NaN with numbers: - const alwaysNaN = Returns(Number, () => NaN) + const alwaysNaN = Returns(NumberT, () => NaN) incremental.merge({add: onType( - [Undefined, Number], alwaysNaN, - [Number, Undefined], alwaysNaN + [Undefined, NumberT], alwaysNaN, + [NumberT, Undefined], alwaysNaN )}) assert(isNaN(incremental.add(undefined, -3.25))) assert.strictEqual( - incremental.add.resolve([Undefined, Number]).returns, - Number) + incremental.add.resolve([Undefined, NumberT]).returns, + NumberT) + assert.strictEqual(incremental.isnan(NaN), 1) + incremental.merge(booleans) + assert.strictEqual(incremental.boolean(undefined), false) + assert.strictEqual(incremental.isnan(NaN), true) }) it('changes methods when their dependencies change', () => { const gnmath = new TypeDispatcher(generics, numbers) @@ -47,4 +52,15 @@ describe('TypeDispatcher', () => { gnmath.merge({multiply: plain((a,b) => Math.floor(a) * b)}) assert.strictEqual(gnmath.square(-2.5), 7.5) }) + it('detects dependencies on conversion operations', () => { + const bgn = new TypeDispatcher(booleans, generics, numbers) + const {BooleanT, NumberT} = bgn.types + assert(!bgn._behaviors.negate.has([BooleanT])) + assert.strictEqual(bgn.negate(true), -1) + assert(bgn._behaviors.negate.has([BooleanT])) + const deps = bgn._dependencies.negate + bgn.merge({number: onType([BooleanT], Returns(NumberT, b => b ? 2 : 0))}) + assert(!bgn._behaviors.negate.has([BooleanT])) + assert.strictEqual(bgn.negate(true), -2) + }) }) diff --git a/src/core/__test__/TypePatterns.spec.js b/src/core/__test__/TypePatterns.spec.js index 0a081ea..5aefc3a 100644 --- a/src/core/__test__/TypePatterns.spec.js +++ b/src/core/__test__/TypePatterns.spec.js @@ -1,5 +1,7 @@ import assert from 'assert' -import {pattern, Any, Multiple, Optional} from '../TypePatterns.js' +import { + pattern, Any, Multiple, Optional, needsCollection +} from '../TypePatterns.js' import {Undefined, TypeOfTypes} from '../Type.js' describe('Type patterns', () => { @@ -93,4 +95,10 @@ describe('Type patterns', () => { whyNot.sampleTypes(), [Undefined, TypeOfTypes]) assert(whyNot.equal(whyNot)) }) + it('determines whether a template needs a collection function', () => { + assert(!needsCollection([Undefined])) + assert(needsCollection([Undefined, [Undefined, Undefined]])) + assert(needsCollection( + [Undefined, {actual: Undefined, matched: TypeOfTypes}])) + }) }) diff --git a/src/core/__test__/helpers.spec.js b/src/core/__test__/helpers.spec.js index 50969bc..c8321d8 100644 --- a/src/core/__test__/helpers.spec.js +++ b/src/core/__test__/helpers.spec.js @@ -9,8 +9,8 @@ describe('Core helpers', () => { it('defines what Implementations are', () => { const imps = onType(Undefined, 7, [TypeOfTypes, Undefined], -3) assert(imps instanceof Implementations) - assert(imps.patterns instanceof Map) - assert(imps.patterns.keys().every(k => k instanceof TypePattern)) + assert(imps.patterns instanceof Array) + assert(imps.patterns.every(([k]) => k instanceof TypePattern)) }) it('detects plain objects', () => { diff --git a/src/core/helpers.js b/src/core/helpers.js index 7d12fe0..ce000e5 100644 --- a/src/core/helpers.js +++ b/src/core/helpers.js @@ -1,13 +1,18 @@ -import {Type} from './Type.js' import {pattern} from './TypePatterns.js' export class Implementations { constructor(imps) { - this.patterns = new Map() + this.patterns = [] + this._add(imps) + } + _add(imps) { for (let i = 0; i < imps.length; ++i) { - this.patterns.set(pattern(imps[i]), imps[++i]) + this.patterns.push([pattern(imps[i]), imps[++i]]) } } + also(...imps) { + this._add(imps) + } } export const onType = (...imps) => new Implementations(imps) diff --git a/src/generic/__test__/arithmetic.spec.js b/src/generic/__test__/arithmetic.spec.js index f1d6043..6ccb17c 100644 --- a/src/generic/__test__/arithmetic.spec.js +++ b/src/generic/__test__/arithmetic.spec.js @@ -5,7 +5,7 @@ describe('generic arithmetic', () => { it('squares anything', () => { assert.strictEqual(math.square(7), 49) assert.strictEqual( - math.square.resolve([math.types.Number]).returns, - math.types.Number) + math.square.resolve([math.types.NumberT]).returns, + math.types.NumberT) }) }) diff --git a/src/generics.js b/src/generics.js deleted file mode 100644 index 0ce5d7b..0000000 --- a/src/generics.js +++ /dev/null @@ -1 +0,0 @@ -export * as generics from './generic/all.js' diff --git a/src/nanomath.js b/src/nanomath.js index c32247d..3951ef4 100644 --- a/src/nanomath.js +++ b/src/nanomath.js @@ -1,7 +1,8 @@ -import {generics} from './generics.js' -import {numbers} from './numbers.js' +import * as booleans from './boolean/all.js' +import * as generics from './generic/all.js' +import * as numbers from './number/all.js' import {TypeDispatcher} from '#core/TypeDispatcher.js' -const math = new TypeDispatcher(generics, numbers) +const math = new TypeDispatcher(booleans, generics, numbers) export default math diff --git a/src/number/Number.js b/src/number/Number.js deleted file mode 100644 index c62c818..0000000 --- a/src/number/Number.js +++ /dev/null @@ -1,3 +0,0 @@ -import {Type} from '#core/Type.js' - -export const Number = new Type(n => typeof n === 'number') diff --git a/src/number/NumberT.js b/src/number/NumberT.js new file mode 100644 index 0000000..99c53ff --- /dev/null +++ b/src/number/NumberT.js @@ -0,0 +1,7 @@ +import {Type} from '#core/Type.js' +import {onType} from '#core/helpers.js' +import {BooleanT} from '#boolean/BooleanT.js' + +export const NumberT = new Type(n => typeof n === 'number', { + from: onType(BooleanT, math => math.number.resolve([BooleanT])), +}) diff --git a/src/number/__test__/Number.spec.js b/src/number/__test__/Number.spec.js deleted file mode 100644 index 5171642..0000000 --- a/src/number/__test__/Number.spec.js +++ /dev/null @@ -1,11 +0,0 @@ -import assert from 'assert' -import {Number} from '../Number.js' - -describe('Number Type', () => { - it('correctly recognizes numbers', () => { - assert(Number.test(3)) - assert(Number.test(NaN)) - assert(Number.test(Infinity)) - assert(!Number.test("3")) - }) -}) diff --git a/src/number/__test__/NumberT.spec.js b/src/number/__test__/NumberT.spec.js new file mode 100644 index 0000000..008dcad --- /dev/null +++ b/src/number/__test__/NumberT.spec.js @@ -0,0 +1,26 @@ +import assert from 'assert' +import {NumberT} from '../NumberT.js' +import {BooleanT} from '#boolean/BooleanT.js' +import math from '#nanomath' + +describe('NumberT Type', () => { + it('correctly recognizes numbers', () => { + assert(NumberT.test(3)) + assert(NumberT.test(NaN)) + assert(NumberT.test(Infinity)) + assert(!NumberT.test("3")) + }) + + it('can convert from BooleanT to NumberT', () => { + const convertImps = NumberT.from + let cnvBtoN + for (const [pattern, convFactory] of convertImps.patterns) { + if (pattern.match([BooleanT])) { + cnvBtoN = convFactory(math) + break + } + } + assert.strictEqual(cnvBtoN(true), 1) + assert.strictEqual(cnvBtoN(false), 0) + }) +}) diff --git a/src/number/__test__/type.spec.js b/src/number/__test__/type.spec.js index 17cf656..bd07690 100644 --- a/src/number/__test__/type.spec.js +++ b/src/number/__test__/type.spec.js @@ -4,5 +4,9 @@ import math from '#nanomath' describe('number type operations', () => { it('converts to number', () => { assert.strictEqual(math.number(2.637), 2.637) + assert(isNaN(math.number(NaN))) + assert.strictEqual(math.number(true), 1) + assert.strictEqual(math.number(false), 0) + assert.strictEqual(math.number(), 0) }) }) diff --git a/src/number/__test__/utils.spec.js b/src/number/__test__/utils.spec.js index 50651b0..054a30a 100644 --- a/src/number/__test__/utils.spec.js +++ b/src/number/__test__/utils.spec.js @@ -5,4 +5,9 @@ describe('number utilities', () => { it('clones a number', () => { assert.strictEqual(math.clone(2.637), 2.637) }) + it('tests if a number is NaN', () => { + assert.strictEqual(math.isnan(NaN), true) + assert.strictEqual(math.isnan(Infinity), false) + assert.strictEqual(math.isnan(43), false) + }) }) diff --git a/src/number/all.js b/src/number/all.js index d82eb49..1de3c09 100644 --- a/src/number/all.js +++ b/src/number/all.js @@ -1,4 +1,4 @@ -export * as typeDefinition from './Number.js' +export * as typeDefinition from './NumberT.js' export * as arithmetic from './arithmetic.js' export * as type from './type.js' export * as utils from './utils.js' diff --git a/src/number/helpers.js b/src/number/helpers.js index 8222ed6..faee3ed 100644 --- a/src/number/helpers.js +++ b/src/number/helpers.js @@ -1,7 +1,9 @@ -import {Number} from './Number.js' +import {NumberT} from './NumberT.js' import {onType} from '#core/helpers.js' import {Returns} from '#core/Type.js' export const plain = f => onType( - Array(f.length).fill(Number), Returns(Number, f)) + Array(f.length).fill(NumberT), Returns(NumberT, f)) + +export const boolnum = Returns(NumberT, p => p ? 1 : 0) diff --git a/src/number/type.js b/src/number/type.js index 4a963c9..0c279da 100644 --- a/src/number/type.js +++ b/src/number/type.js @@ -1,4 +1,12 @@ -import {plain} from './helpers.js' +import {plain, boolnum} from './helpers.js' +import {BooleanT} from '#boolean/BooleanT.js' +import {Returns} from '#core/Type.js' +import {NumberT} from '#number/NumberT.js' + +const num = f => Returns(NumberT, f) -// Not much to do so far when there is only one type export const number = plain(a => a) +number.also( + BooleanT, boolnum, + [], num(() => 0) +) diff --git a/src/number/utils.js b/src/number/utils.js index 783d6c3..06973df 100644 --- a/src/number/utils.js +++ b/src/number/utils.js @@ -1,3 +1,12 @@ -import {plain} from './helpers.js' +import {plain, boolnum} from './helpers.js' +import {NumberT} from './NumberT.js' + +import {Returns} from '#core/Type.js' +import {onType} from '#core/helpers.js' export const clone = plain(a => a) +export const isnan = onType(NumberT, math => { + const {BooleanT} = math.types + if (BooleanT) return Returns(BooleanT, a => isNaN(a)) + return Returns(NumberT, a => boolnum(isNaN(a))) +}) diff --git a/src/numbers.js b/src/numbers.js index 2f6a5ec..1ee1233 100644 --- a/src/numbers.js +++ b/src/numbers.js @@ -1 +1,7 @@ -export * as numbers from './number/all.js' +import * as numbers from './number/all.js' +import * as generics from './generic/all.js' +import {TypeDispatcher} from '#core/TypeDispatcher.js' + +const math = new TypeDispatcher(numbers, generics) + +export default math diff --git a/src/package.json b/src/package.json index 41ce1cd..290fb63 100644 --- a/src/package.json +++ b/src/package.json @@ -1,7 +1,9 @@ { "imports" : { "#nanomath": "./nanomath.js", + "#boolean/*.js": "./boolean/*.js", "#core/*.js": "./core/*.js", + "#generic/*.js": "./generic/*.js", "#number/*.js": "./number/*.js" }, "type" : "module"