From 5bee93dbb318f13a418e74bb0e292711be122f55 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Thu, 10 Apr 2025 13:57:05 -0700 Subject: [PATCH 1/7] refactor: prepare for boolean functions * Defines a BooleanT type * adds options to the Type constructor, so far just to allow conversions from other types * renames Number type to NumberT * records the name of types, and puts the name in the string representation * defines a conversion fron BooleanT to NumberT, specified to be automatic * stub code for automatic conversions, not yet complete * BooleanT not yet added to nanomath Checked that the new facilities do not disrupt the prior behavior on numbers. --- src/boolean/BooleanT.js | 3 ++ src/core/Type.js | 7 ++- src/core/TypeDispatcher.js | 27 ++++++---- src/core/TypePatterns.js | 66 ++++++++++++++++++------ src/core/__test__/Type.spec.js | 10 ++-- src/core/__test__/TypeDispatcher.spec.js | 18 +++---- src/core/helpers.js | 7 ++- src/generic/__test__/arithmetic.spec.js | 4 +- src/number/Number.js | 3 -- src/number/NumberT.js | 6 +++ src/number/__test__/Number.spec.js | 11 ---- src/number/__test__/NumberT.spec.js | 11 ++++ src/number/all.js | 2 +- src/number/helpers.js | 4 +- src/number/type.js | 5 +- src/package.json | 1 + 16 files changed, 123 insertions(+), 62 deletions(-) create mode 100644 src/boolean/BooleanT.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/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/core/Type.js b/src/core/Type.js index cb3f8b0..46796bd 100644 --- a/src/core/Type.js +++ b/src/core/Type.js @@ -3,8 +3,13 @@ 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 ?? [] + this.from.push(this) + } + toString() { + return this.name || `[Type ${this.test}]` } } diff --git a/src/core/TypeDispatcher.js b/src/core/TypeDispatcher.js index 23e1d54..42cdd66 100644 --- a/src/core/TypeDispatcher.js +++ b/src/core/TypeDispatcher.js @@ -3,9 +3,12 @@ import {typeOf, Type, bootstrapTypes} from './Type.js' import { Implementations, isPlainFunction, isPlainObject, onType } from './helpers.js' -import {alwaysMatches, Any, Multiple} from './TypePatterns.js' +import { + alwaysMatches, matched, needsCollection, Any, Multiple +} from './TypePatterns.js' // helper that organizes a list into the same chunks as a pattern is +// NOTE NOTE: need to update to handle conversions! const collectLike = (list, pattern, state = {pos: 0}) => { const result = [] for (const elt of pattern) { @@ -41,6 +44,7 @@ export class TypeDispatcher { if (val instanceof Type) { // TODO: Need to wipe out any dependencies on types[key]! this.types[key] = val + val.name = key continue } if (typeof val === 'function') { @@ -161,13 +165,16 @@ export class TypeDispatcher { 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}]) { + 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) { @@ -190,7 +197,8 @@ export class TypeDispatcher { this.resolve._genDepsOf.push([key, types]) let theBehavior = () => undefined try { - theBehavior = item(DependencyRecorder(this, [], this), template) + theBehavior = item( + DependencyRecorder(this, [], this), matched(template)) } catch { behave.set(types, item) return item @@ -199,8 +207,7 @@ export class TypeDispatcher { } if (typeof theBehavior === 'function' && theBehavior.length - && Array.isArray(template) - && template.some(elt => Array.isArray(elt)) + && needsCollection(template) ) { // have to wrap the behavior to collect the actual arguments // in the way corresponding to the template diff --git a/src/core/TypePatterns.js b/src/core/TypePatterns.js index 3db4f03..185f387 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,10 +15,16 @@ class MatchTypePattern extends TypePattern { super() this.type = typeToMatch } - match(typeSequence, position = 0) { + match(typeSequence, options={}) { + const position = options.position ?? 0 + const allowed = options.convert ? this.type.from : [this.type] if (position < typeSequence.length - && typeSequence[position] === this.type + && allowed.includes(typeSequence[position]) ) { + let templateItem = typeSequence[position] + if (templateItem !== this.type) { + templateItem = {actual: templateItem, matched: this.type} + } return [position + 1, typeSequence[position]] } return [-1, Undefined] @@ -32,15 +38,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 +72,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 +88,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 +114,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) } } @@ -117,3 +136,18 @@ class MultiPattern extends TypePattern { export const Multiple = item => new MultiPattern(item) export const alwaysMatches = pat => pat instanceof MultiPattern && pat.pattern instanceof AnyPattern + +// 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..120bc0f 100644 --- a/src/core/__test__/TypeDispatcher.spec.js +++ b/src/core/__test__/TypeDispatcher.spec.js @@ -13,8 +13,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 +27,18 @@ 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) }) it('changes methods when their dependencies change', () => { const gnmath = new TypeDispatcher(generics, numbers) diff --git a/src/core/helpers.js b/src/core/helpers.js index 7d12fe0..a681c0e 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._add(imps) + } + _add(imps) { for (let i = 0; i < imps.length; ++i) { this.patterns.set(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/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..22ddbbe --- /dev/null +++ b/src/number/NumberT.js @@ -0,0 +1,6 @@ +import {Type} from '#core/Type.js' +import {BooleanT} from '#boolean/BooleanT.js' + +export const NumberT = new Type(n => typeof n === 'number', { + from: [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..c6afe44 --- /dev/null +++ b/src/number/__test__/NumberT.spec.js @@ -0,0 +1,11 @@ +import assert from 'assert' +import {NumberT} from '../NumberT.js' + +describe('NumberT Type', () => { + it('correctly recognizes numbers', () => { + assert(NumberT.test(3)) + assert(NumberT.test(NaN)) + assert(NumberT.test(Infinity)) + assert(!NumberT.test("3")) + }) +}) 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..d082667 100644 --- a/src/number/helpers.js +++ b/src/number/helpers.js @@ -1,7 +1,7 @@ -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)) diff --git a/src/number/type.js b/src/number/type.js index 4a963c9..b970b5e 100644 --- a/src/number/type.js +++ b/src/number/type.js @@ -1,4 +1,7 @@ import {plain} from './helpers.js' +import {BooleanT} from '#boolean/BooleanT.js' +import {Returns} from '#core/Type.js' +import {NumberT} from '#number/NumberT.js' -// Not much to do so far when there is only one type export const number = plain(a => a) +number.also(BooleanT, Returns(NumberT, a => a ? 1 : 0)) diff --git a/src/package.json b/src/package.json index 41ce1cd..a02544b 100644 --- a/src/package.json +++ b/src/package.json @@ -1,6 +1,7 @@ { "imports" : { "#nanomath": "./nanomath.js", + "#boolean/*.js": "./boolean/*.js", "#core/*.js": "./core/*.js", "#number/*.js": "./number/*.js" }, -- 2.43.0 From bfc64f3789f6a34a6226fe76eb10538a17b9a90b Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Thu, 10 Apr 2025 22:47:30 -0700 Subject: [PATCH 2/7] feat: implicit convert BooleanT to NumberT Also adds implicit conversion configuration option to Type constructor, and institutes a numbers-only bundle, and supports argument matching with implicit conversions. Still need to test that a behavior that invokes implicit conversion ends up with the conversion operation as a dependency (and so regenerates itself if the conversion changes). --- src/__test__/numbers.spec.js | 9 +++++ src/boolean/__test__/BooleanT.spec.js | 24 +++++++++++++ src/boolean/all.js | 1 + src/core/Type.js | 3 +- src/core/TypeDispatcher.js | 46 +++++++++++++++--------- src/core/TypePatterns.js | 13 +++---- src/core/__test__/TypeDispatcher.spec.js | 4 +-- src/generics.js | 1 - src/nanomath.js | 7 ++-- src/number/NumberT.js | 2 +- src/number/__test__/NumberT.spec.js | 8 +++++ src/number/__test__/type.spec.js | 2 ++ src/numbers.js | 8 ++++- src/package.json | 1 + 14 files changed, 95 insertions(+), 34 deletions(-) create mode 100644 src/__test__/numbers.spec.js create mode 100644 src/boolean/__test__/BooleanT.spec.js create mode 100644 src/boolean/all.js delete mode 100644 src/generics.js diff --git a/src/__test__/numbers.spec.js b/src/__test__/numbers.spec.js new file mode 100644 index 0000000..effd3dd --- /dev/null +++ b/src/__test__/numbers.spec.js @@ -0,0 +1,9 @@ +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) + }) +}) 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/all.js b/src/boolean/all.js new file mode 100644 index 0000000..f133295 --- /dev/null +++ b/src/boolean/all.js @@ -0,0 +1 @@ +export * as typeDefinition from './BooleanT.js' diff --git a/src/core/Type.js b/src/core/Type.js index 46796bd..e527f40 100644 --- a/src/core/Type.js +++ b/src/core/Type.js @@ -5,8 +5,7 @@ export const types = () => typeObject export class Type { constructor(f, options = {}) { this.test = f - this.from = options.from ?? [] - this.from.push(this) + this.from = new Map(options.from ?? []) } toString() { return this.name || `[Type ${this.test}]` diff --git a/src/core/TypeDispatcher.js b/src/core/TypeDispatcher.js index 42cdd66..76eace6 100644 --- a/src/core/TypeDispatcher.js +++ b/src/core/TypeDispatcher.js @@ -7,17 +7,6 @@ import { alwaysMatches, matched, needsCollection, Any, Multiple } from './TypePatterns.js' -// helper that organizes a list into the same chunks as a pattern is -// NOTE NOTE: need to update to handle conversions! -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 -} - export class TypeDispatcher { constructor(...specs) { this._implementations = {} // maps key to list of [pattern, result] pairs @@ -150,6 +139,26 @@ 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 + const convert = elt.matched.from.get(elt.actual)(this) + 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}'`) @@ -200,6 +209,8 @@ export class TypeDispatcher { 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: behave.set(types, item) return item } finally { @@ -210,12 +221,15 @@ export class TypeDispatcher { && needsCollection(template) ) { // 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) + // in the way corresponding to the template. That may generate + // more dependencies: + this.resolve._genDepsOf.push([key, types]) + try { + const collectFunction = this._generateCollectFunction(template) + theBehavior = (...args) => theBehavior(...collectFunction(args)) + } finally { + this.resolve._genDepsOf.pop() } - theBehavior = wrappedBehavior } behave.set(types, theBehavior) return theBehavior diff --git a/src/core/TypePatterns.js b/src/core/TypePatterns.js index 185f387..0af2fb0 100644 --- a/src/core/TypePatterns.js +++ b/src/core/TypePatterns.js @@ -17,15 +17,12 @@ class MatchTypePattern extends TypePattern { } match(typeSequence, options={}) { const position = options.position ?? 0 - const allowed = options.convert ? this.type.from : [this.type] - if (position < typeSequence.length - && allowed.includes(typeSequence[position]) - ) { - let templateItem = typeSequence[position] - if (templateItem !== this.type) { - templateItem = {actual: templateItem, matched: this.type} + const actual = typeSequence[position] + if (position < typeSequence.length) { + if (actual === this.type) return [position + 1, actual] + if (options.convert && this.type.from.has(actual)) { + return [position + 1, {actual, matched: this.type}] } - return [position + 1, typeSequence[position]] } return [-1, Undefined] } diff --git a/src/core/__test__/TypeDispatcher.spec.js b/src/core/__test__/TypeDispatcher.spec.js index 120bc0f..3cb35e4 100644 --- a/src/core/__test__/TypeDispatcher.spec.js +++ b/src/core/__test__/TypeDispatcher.spec.js @@ -1,7 +1,7 @@ import assert from 'assert' import {TypeDispatcher} from '../TypeDispatcher.js' -import {numbers} from '../../numbers.js' -import {generics} from '../../generics.js' +import * as numbers from '#number/all.js' +import * as generics from '#generic/all.js' import {onType} from "#core/helpers.js" import {Any} from "#core/TypePatterns.js" import {Returns} from "#core/Type.js" 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/NumberT.js b/src/number/NumberT.js index 22ddbbe..0a08feb 100644 --- a/src/number/NumberT.js +++ b/src/number/NumberT.js @@ -2,5 +2,5 @@ import {Type} from '#core/Type.js' import {BooleanT} from '#boolean/BooleanT.js' export const NumberT = new Type(n => typeof n === 'number', { - from: [BooleanT] + from: [[BooleanT, math => math.number.resolve([BooleanT])]], }) diff --git a/src/number/__test__/NumberT.spec.js b/src/number/__test__/NumberT.spec.js index c6afe44..9e09131 100644 --- a/src/number/__test__/NumberT.spec.js +++ b/src/number/__test__/NumberT.spec.js @@ -1,5 +1,7 @@ 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', () => { @@ -8,4 +10,10 @@ describe('NumberT Type', () => { assert(NumberT.test(Infinity)) assert(!NumberT.test("3")) }) + + it('can convert from BooleanT to NumberT', () => { + const cnvBtoN = NumberT.from.get(BooleanT)(math) + 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..9d05744 100644 --- a/src/number/__test__/type.spec.js +++ b/src/number/__test__/type.spec.js @@ -4,5 +4,7 @@ import math from '#nanomath' describe('number type operations', () => { it('converts to number', () => { assert.strictEqual(math.number(2.637), 2.637) + assert.strictEqual(math.number(true), 1) + assert.strictEqual(math.number(false), 0) }) }) 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 a02544b..290fb63 100644 --- a/src/package.json +++ b/src/package.json @@ -3,6 +3,7 @@ "#nanomath": "./nanomath.js", "#boolean/*.js": "./boolean/*.js", "#core/*.js": "./core/*.js", + "#generic/*.js": "./generic/*.js", "#number/*.js": "./number/*.js" }, "type" : "module" -- 2.43.0 From f38a2d5e8892ebc81e087decf97a396cdb7899fb Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sat, 12 Apr 2025 00:07:18 -0700 Subject: [PATCH 3/7] test: ensure that a function requiring conversion depends on converter This test turned out to be a very discerning one. Adding it uncovered numerous bugs, large and small, in the TypeDispatcher. The most major one was that a Map distinguishes keys by strict equality, and hence since every lookup for a cached behavior was using a newly-generated array of types, the cache was never being hit. So it looked like methods were being updated as dependencies changed, when what was really happening was that the behavior was simply being regenerated from scratch on every call, which would not be performant as the prototype scaled. This bug is now fixed (by switching to a third-party ArrayKeyedMap), along with many smaller bugs too numerous to list. It should now be feasible to go through Pocomath and add all of the functions that depend on numbers and booleans only. --- package.json5 | 3 + pnpm-lock.yaml | 9 ++ src/core/Type.js | 5 +- src/core/TypeDispatcher.js | 128 ++++++++++++++--------- src/core/TypePatterns.js | 23 +++- src/core/__test__/TypeDispatcher.spec.js | 14 ++- src/core/__test__/TypePatterns.spec.js | 10 +- src/core/__test__/helpers.spec.js | 4 +- src/core/helpers.js | 4 +- src/number/NumberT.js | 3 +- src/number/__test__/NumberT.spec.js | 9 +- 11 files changed, 151 insertions(+), 61 deletions(-) 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/core/Type.js b/src/core/Type.js index e527f40..70e1ed1 100644 --- a/src/core/Type.js +++ b/src/core/Type.js @@ -1,3 +1,5 @@ +import {onType} from './helpers.js' + const typeObject = {} // have to make sure there is only one export const types = () => typeObject @@ -5,7 +7,8 @@ export const types = () => typeObject export class Type { constructor(f, options = {}) { this.test = f - this.from = new Map(options.from ?? []) + this.from = options.from ?? onType() // empty Implementations if no ... + // ... conversions specified } toString() { return this.name || `[Type ${this.test}]` diff --git a/src/core/TypeDispatcher.js b/src/core/TypeDispatcher.js index 76eace6..8c495d5 100644 --- a/src/core/TypeDispatcher.js +++ b/src/core/TypeDispatcher.js @@ -1,11 +1,10 @@ -import {typeOf, Type, bootstrapTypes} from './Type.js' +import ArrayKeyedMap from 'array-keyed-map' import { Implementations, isPlainFunction, isPlainObject, onType } from './helpers.js' -import { - alwaysMatches, matched, needsCollection, Any, Multiple -} from './TypePatterns.js' +import {bootstrapTypes, Returns, typeOf, Type} from './Type.js' +import {matched, needsCollection, Passthru} from './TypePatterns.js' export class TypeDispatcher { constructor(...specs) { @@ -37,25 +36,39 @@ export class TypeDispatcher { 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: @@ -74,7 +87,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) @@ -92,6 +105,7 @@ export class TypeDispatcher { }) return get() } + Object.defineProperty(this, key, { enumerable: true, configurable: true, @@ -100,13 +114,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 { @@ -128,7 +145,7 @@ export class TypeDispatcher { } // install value as a catchall value - this.merge({[key]: onType(Multiple(Any), val)}) + this.merge({[key]: onType(Passthru, val)}) } } @@ -151,7 +168,10 @@ export class TypeDispatcher { } else { const from = state.pos++ if ('actual' in elt) { // incorporate conversion - const convert = elt.matched.from.get(elt.actual)(this) + 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]) } @@ -167,14 +187,17 @@ 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 (const options of [{}, {convert: true}]) { + let pattern for ([pattern, item] of imps) { let finalIndex ;[finalIndex, template] = pattern.match(types, options) @@ -189,50 +212,59 @@ export class TypeDispatcher { 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), 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: - behave.set(types, item) - return item - } finally { - this.resolve._genDepsOf.pop() - } - if (typeof theBehavior === 'function' - && theBehavior.length - && needsCollection(template) - ) { - // have to wrap the behavior to collect the actual arguments - // in the way corresponding to the template. That may generate - // more dependencies: - this.resolve._genDepsOf.push([key, types]) + this.resolve._genDepsOf.push([key, types]) // Important: make sure + // not to return without popping _genDepsOf + if (!('returns' in item)) { + // looks like a factory try { - const collectFunction = this._generateCollectFunction(template) - theBehavior = (...args) => theBehavior(...collectFunction(args)) - } finally { - this.resolve._genDepsOf.pop() + 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))) } } - 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 diff --git a/src/core/TypePatterns.js b/src/core/TypePatterns.js index 0af2fb0..efe05f8 100644 --- a/src/core/TypePatterns.js +++ b/src/core/TypePatterns.js @@ -20,8 +20,13 @@ class MatchTypePattern extends TypePattern { const actual = typeSequence[position] if (position < typeSequence.length) { if (actual === this.type) return [position + 1, actual] - if (options.convert && this.type.from.has(actual)) { - return [position + 1, {actual, matched: this.type}] + 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] @@ -131,8 +136,18 @@ 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) => { diff --git a/src/core/__test__/TypeDispatcher.spec.js b/src/core/__test__/TypeDispatcher.spec.js index 3cb35e4..62a997e 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 * as numbers from '#number/all.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" @@ -47,4 +48,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 a681c0e..ce000e5 100644 --- a/src/core/helpers.js +++ b/src/core/helpers.js @@ -2,12 +2,12 @@ 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) { diff --git a/src/number/NumberT.js b/src/number/NumberT.js index 0a08feb..99c53ff 100644 --- a/src/number/NumberT.js +++ b/src/number/NumberT.js @@ -1,6 +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: [[BooleanT, math => math.number.resolve([BooleanT])]], + from: onType(BooleanT, math => math.number.resolve([BooleanT])), }) diff --git a/src/number/__test__/NumberT.spec.js b/src/number/__test__/NumberT.spec.js index 9e09131..008dcad 100644 --- a/src/number/__test__/NumberT.spec.js +++ b/src/number/__test__/NumberT.spec.js @@ -12,7 +12,14 @@ describe('NumberT Type', () => { }) it('can convert from BooleanT to NumberT', () => { - const cnvBtoN = NumberT.from.get(BooleanT)(math) + 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) }) -- 2.43.0 From 4b81fbe6e2ec273b85e0074ea03d34e2d46208c7 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sat, 12 Apr 2025 07:46:15 -0700 Subject: [PATCH 4/7] feat: add conversions to boolean and make mandatory --- src/boolean/all.js | 1 + src/number/__test__/type.spec.js | 2 ++ src/number/type.js | 7 ++++++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/boolean/all.js b/src/boolean/all.js index f133295..443a8fa 100644 --- a/src/boolean/all.js +++ b/src/boolean/all.js @@ -1 +1,2 @@ export * as typeDefinition from './BooleanT.js' +export * as type from './type.js' diff --git a/src/number/__test__/type.spec.js b/src/number/__test__/type.spec.js index 9d05744..bd07690 100644 --- a/src/number/__test__/type.spec.js +++ b/src/number/__test__/type.spec.js @@ -4,7 +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/type.js b/src/number/type.js index b970b5e..b7656ba 100644 --- a/src/number/type.js +++ b/src/number/type.js @@ -3,5 +3,10 @@ import {BooleanT} from '#boolean/BooleanT.js' import {Returns} from '#core/Type.js' import {NumberT} from '#number/NumberT.js' +const num = f => Returns(NumberT, f) + export const number = plain(a => a) -number.also(BooleanT, Returns(NumberT, a => a ? 1 : 0)) +number.also( + BooleanT, num(a => a ? 1 : 0), + [], num(() => 0) +) -- 2.43.0 From a7673216c1087989ea74b4ee1f12b2f37c9bbf58 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sun, 13 Apr 2025 00:14:56 -0700 Subject: [PATCH 5/7] chore: add boolean test files omitted earlier --- src/boolean/__test__/type.spec.js | 25 +++++++++++++++++++++++++ src/boolean/type.js | 14 ++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 src/boolean/__test__/type.spec.js create mode 100644 src/boolean/type.js 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/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) +) -- 2.43.0 From b2b41d6348c283c20bfa73629d1449909ebf0e5b Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sun, 13 Apr 2025 01:35:10 -0700 Subject: [PATCH 6/7] feat: factories can depend on the presence of types Refactors each TypeDispatcher to have its own separate collection of types. Add isnan function which returns a boolean if that type is present, otherwise returns the number 1 for true, and 0 for fase. --- src/__test__/numbers.spec.js | 4 ++++ src/core/Type.js | 12 +++++------- src/core/TypeDispatcher.js | 20 ++++++++++++++------ src/core/__test__/TypeDispatcher.spec.js | 1 + src/number/__test__/utils.spec.js | 5 +++++ src/number/helpers.js | 2 ++ src/number/type.js | 4 ++-- src/number/utils.js | 11 ++++++++++- 8 files changed, 43 insertions(+), 16 deletions(-) diff --git a/src/__test__/numbers.spec.js b/src/__test__/numbers.spec.js index effd3dd..95f3658 100644 --- a/src/__test__/numbers.spec.js +++ b/src/__test__/numbers.spec.js @@ -6,4 +6,8 @@ describe('the numbers-only bundle', () => { 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/core/Type.js b/src/core/Type.js index 70e1ed1..18530bc 100644 --- a/src/core/Type.js +++ b/src/core/Type.js @@ -1,9 +1,5 @@ import {onType} from './helpers.js' -const typeObject = {} // have to make sure there is only one - -export const types = () => typeObject - export class Type { constructor(f, options = {}) { this.test = f @@ -20,8 +16,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 } @@ -34,10 +30,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 8c495d5..963f033 100644 --- a/src/core/TypeDispatcher.js +++ b/src/core/TypeDispatcher.js @@ -3,16 +3,18 @@ import ArrayKeyedMap from 'array-keyed-map' import { Implementations, isPlainFunction, isPlainObject, onType } from './helpers.js' -import {bootstrapTypes, Returns, typeOf, Type} from './Type.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._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) } @@ -73,9 +75,10 @@ export class TypeDispatcher { // 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) @@ -320,7 +323,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: @@ -331,8 +338,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 } @@ -358,7 +366,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/__test__/TypeDispatcher.spec.js b/src/core/__test__/TypeDispatcher.spec.js index 62a997e..8c5bf81 100644 --- a/src/core/__test__/TypeDispatcher.spec.js +++ b/src/core/__test__/TypeDispatcher.spec.js @@ -40,6 +40,7 @@ describe('TypeDispatcher', () => { assert.strictEqual( incremental.add.resolve([Undefined, NumberT]).returns, NumberT) + assert.strictEqual(incremental.isnan(NaN), 1) }) it('changes methods when their dependencies change', () => { const gnmath = new TypeDispatcher(generics, numbers) 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/helpers.js b/src/number/helpers.js index d082667..faee3ed 100644 --- a/src/number/helpers.js +++ b/src/number/helpers.js @@ -5,3 +5,5 @@ import {Returns} from '#core/Type.js' export const plain = f => onType( 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 b7656ba..0c279da 100644 --- a/src/number/type.js +++ b/src/number/type.js @@ -1,4 +1,4 @@ -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' @@ -7,6 +7,6 @@ const num = f => Returns(NumberT, f) export const number = plain(a => a) number.also( - BooleanT, num(a => a ? 1 : 0), + 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))) +}) -- 2.43.0 From 447c62eae9e427a76b0ca35dabaf1eb1b2760485 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sun, 13 Apr 2025 02:00:20 -0700 Subject: [PATCH 7/7] fix: clear behaviors that depend on an object property --- src/core/Type.js | 5 +---- src/core/TypeDispatcher.js | 8 ++++++-- src/core/__test__/TypeDispatcher.spec.js | 3 +++ 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/core/Type.js b/src/core/Type.js index 18530bc..7c728a2 100644 --- a/src/core/Type.js +++ b/src/core/Type.js @@ -1,10 +1,7 @@ -import {onType} from './helpers.js' - export class Type { constructor(f, options = {}) { this.test = f - this.from = options.from ?? onType() // empty Implementations if no ... - // ... conversions specified + this.from = options.from ?? {patterns: []} // mock empty Implementations } toString() { return this.name || `[Type ${this.test}]` diff --git a/src/core/TypeDispatcher.js b/src/core/TypeDispatcher.js index 963f033..64757ce 100644 --- a/src/core/TypeDispatcher.js +++ b/src/core/TypeDispatcher.js @@ -32,7 +32,11 @@ 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 @@ -353,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: diff --git a/src/core/__test__/TypeDispatcher.spec.js b/src/core/__test__/TypeDispatcher.spec.js index 8c5bf81..81d91a3 100644 --- a/src/core/__test__/TypeDispatcher.spec.js +++ b/src/core/__test__/TypeDispatcher.spec.js @@ -41,6 +41,9 @@ describe('TypeDispatcher', () => { 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) -- 2.43.0