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"