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" },