diff --git a/src/core/Type.js b/src/core/Type.js index 25acbbc..cb3f8b0 100644 --- a/src/core/Type.js +++ b/src/core/Type.js @@ -1,9 +1,4 @@ -import {Returns} from './helpers.js' - -// Order matters here, because these items will merged into a new -// dispatcher in the order they are listed - -const typeObject = {} +const typeObject = {} // have to make sure there is only one export const types = () => typeObject @@ -16,11 +11,20 @@ export class Type { export const Undefined = new Type(t => typeof t === 'undefined') 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)) { if (!(type instanceof Type)) continue if (type.test(item)) return type } + let errorMsg = '' + try { + errorMsg = `no known type for '${item}'` // fails for Symbols, e.g. + } catch { + errorMsg = `no known type for '${item.toString()}'` + } + throw new TypeError(errorMsg) }) // bootstrapping order matters, but order of exports in a module isn't diff --git a/src/core/TypeDispatcher.js b/src/core/TypeDispatcher.js index b0a28ec..b763fde 100644 --- a/src/core/TypeDispatcher.js +++ b/src/core/TypeDispatcher.js @@ -1,6 +1,8 @@ import {typeOf, Type, bootstrapTypes} from './Type.js' -import {Implementations, isPlainObject, onType} from './helpers.js' +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 @@ -64,7 +66,7 @@ export class TypeDispatcher { } // Redefine the property according to what sort of // entity it is: - if (typeof tryValue === 'function') { + if (isPlainFunction(tryValue)) { // the usual case: a method of the dispatcher const standard = (...args) => { const types = args.map(typeOf) @@ -161,7 +163,7 @@ export class TypeDispatcher { if (imps.length) { for ([pattern, item] of imps) { let finalIndex - ;[finalIndex, template] = pattern.match(types, 0) + ;[finalIndex, template] = pattern.match(types) if (finalIndex === types.length) { needItem = false break @@ -176,7 +178,7 @@ export class TypeDispatcher { if (needItem) { throw new TypeError(`no matching definition of '${key}' on '${types}'`) } - if (typeof item !== 'function' || 'returns' in item) { + if (!isPlainFunction(item) || 'returns' in item) { behave.set(types, item) return item } @@ -186,8 +188,17 @@ export class TypeDispatcher { this.resolve._genDepsOf = [] } this.resolve._genDepsOf.push([key, types]) - let theBehavior = item(DependencyRecorder(this, [], this), pattern) - this.resolve._genDepsOf.pop() + let theBehavior = () => undefined + try { + theBehavior = item(DependencyRecorder(this, [], this), template) + } catch { + // Didn't work as a factory, so guess we were wrong; just + // make this entity the behavior: + behave.set(types, item) + return item + } finally { + this.resolve._genDepsOf.pop() + } if (typeof theBehavior === 'function' && theBehavior.length && template.some(elt => Array.isArray(elt)) diff --git a/src/core/TypePatterns.js b/src/core/TypePatterns.js index 1561827..3db4f03 100644 --- a/src/core/TypePatterns.js +++ b/src/core/TypePatterns.js @@ -1,7 +1,7 @@ import {Type, Undefined} from './Type.js' -class TypePattern { - match(typeSequence, position) { +export class TypePattern { + match(typeSequence, position = 0) { throw new Error('Specific TypePatterns must implement match') } sampleTypes() { @@ -15,7 +15,7 @@ class MatchTypePattern extends TypePattern { super() this.type = typeToMatch } - match(typeSequence, position) { + match(typeSequence, position = 0) { if (position < typeSequence.length && typeSequence[position] === this.type ) { @@ -32,7 +32,7 @@ class SequencePattern extends TypePattern { super() this.patterns = itemsToMatch.map(pattern) } - match(typeSequence, position) { + match(typeSequence, position = 0) { const matches = [] for (const pat of this.patterns) { const [newPos, newMatch] = pat.match(typeSequence, position) @@ -62,7 +62,7 @@ export const pattern = patternOrSpec => { } class AnyPattern extends TypePattern { - match(typeSequence, position) { + match(typeSequence, position = 0) { return position < typeSequence.length ? [position + 1, typeSequence[position]] : [-1, Undefined] @@ -74,9 +74,10 @@ export const Any = new AnyPattern() class OptionalPattern extends TypePattern { constructor(item) { + super() this.pattern = pattern(item) } - match(typeSequence, position) { + match(typeSequence, position = 0) { const matches = [] const [newPos, newMatch] = this.pattern.match(typeSequence, position) if (newPos >= 0) { @@ -98,7 +99,7 @@ class MultiPattern extends TypePattern { super() this.pattern = pattern(item) } - match(typeSequence, position) { + match(typeSequence, position = 0) { const matches = [] while (true) { const [newPos, newMatch] = this.pattern.match(typeSequence, position) diff --git a/src/core/__test__/Type.spec.js b/src/core/__test__/Type.spec.js new file mode 100644 index 0000000..7563672 --- /dev/null +++ b/src/core/__test__/Type.spec.js @@ -0,0 +1,43 @@ +import assert from 'assert' +import math from '#nanomath' +import {Number} from '#number/Number.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('Undefined' in math.types) + assert(!('Type' in math.types)) + assert(math.types.Undefined.test(undefined)) + assert(!math.types.Undefined.test(3)) + assert(math.types.TypeOfTypes.test(math.types.Undefined)) + }) + + it('supports a typeOf operator', () => { + const tO = math.typeOf + assert.strictEqual(tO(7), Number) + assert.strictEqual(tO(undefined), math.types.Undefined) + assert.strictEqual(tO(Number), math.types.TypeOfTypes) + assert.throws(() => tO(Symbol()), TypeError) + }) + + it('lets you create a new Type', () => { + const Foo = new math.Type( + item => item && typeof item === 'object' && 'bar' in item) + assert(Foo.test({bar: 'baz'})) + math.merge({Foo}) + assert.strictEqual(math.types.Foo, Foo) + assert.strictEqual(math.typeOf({gold: 7, bar: 3}), Foo) + }) + + it('provides a return-value labeling', () => { + const labeledF = Returns(math.types.Undefined, () => undefined) + assert.strictEqual(typeof labeledF, 'function') + assert.strictEqual(labeledF.returns, math.types.Undefined) + assert(isPlainFunction(labeledF)) + }) + +}) diff --git a/src/core/__test__/TypeDispatcher.spec.js b/src/core/__test__/TypeDispatcher.spec.js new file mode 100644 index 0000000..2a61b17 --- /dev/null +++ b/src/core/__test__/TypeDispatcher.spec.js @@ -0,0 +1,41 @@ +import assert from 'assert' +import {TypeDispatcher} from '../TypeDispatcher.js' +import {numbers} from '../../numbers.js' +import {onType} from "#core/helpers.js" +import {Any} from "#core/TypePatterns.js" +import {Returns} from "#core/Type.js" + +describe('TypeDispatcher', () => { + it('can build in stages', () => { + const incremental = new TypeDispatcher() + assert.strictEqual( + incremental.typeOf(undefined), incremental.types.Undefined) + incremental.merge(numbers) + const {Number, TypeOfTypes, Undefined} = incremental.types + assert(Number.test(7)) + assert.strictEqual(incremental.add(-1.5, 0.5), -1) + // Make Undefined act like zero: + incremental.merge({add: onType( + [Undefined, Any], (_m, [_U, T]) => Returns(T, (_a, b) => b), + [Any, Undefined], (_m, [T]) => Returns(T, a => a) + )}) + assert.strictEqual(incremental.add(7, undefined), 7) + assert.strictEqual( + incremental.resolve('add', [TypeOfTypes, Undefined]).returns, + TypeOfTypes) + assert.strictEqual(incremental.add(undefined, -3.25), -3.25) + assert.strictEqual( + incremental.add.resolve([Undefined, Number]).returns, + Number) + // Oops, changed my mind, make it work like NaN with numbers: + const alwaysNaN = Returns(Number, () => NaN) + incremental.merge({add: onType( + [Undefined, Number], alwaysNaN, + [Number, Undefined], alwaysNaN + )}) + assert(isNaN(incremental.add(undefined, -3.25))) + assert.strictEqual( + incremental.add.resolve([Undefined, Number]).returns, + Number) + }) +}) diff --git a/src/core/__test__/TypePatterns.spec.js b/src/core/__test__/TypePatterns.spec.js new file mode 100644 index 0000000..0a081ea --- /dev/null +++ b/src/core/__test__/TypePatterns.spec.js @@ -0,0 +1,96 @@ +import assert from 'assert' +import {pattern, Any, Multiple, Optional} from '../TypePatterns.js' +import {Undefined, TypeOfTypes} from '../Type.js' + +describe('Type patterns', () => { + it('makes patterns from types and sequences thereof', () => { + const undef1 = pattern(Undefined) + assert.deepStrictEqual(undef1.match([Undefined]), [1, Undefined]) + assert.deepStrictEqual( + undef1.match([Undefined, Undefined]), [1, Undefined]) + assert.deepStrictEqual(undef1.match([TypeOfTypes]), [-1, Undefined]) + assert.deepStrictEqual(undef1.match([]), [-1, Undefined]) + assert.deepStrictEqual(undef1.sampleTypes(), [Undefined]) + assert(undef1.equal(pattern(Undefined))) + assert(!undef1.equal(pattern(TypeOfTypes))) + const tu2 = pattern([TypeOfTypes, undef1]) + assert.deepStrictEqual( + tu2.match([Undefined, TypeOfTypes]), [-1, Undefined]) + assert.deepStrictEqual( + tu2.match([TypeOfTypes, Undefined]), [2, [TypeOfTypes, Undefined]]) + }) + + it('allows Any as a wildcard', () => { + const midWild = pattern([Undefined, Any, TypeOfTypes]) + assert.deepStrictEqual( + midWild.match([Undefined, Undefined, TypeOfTypes]), + [3, [Undefined, Undefined, TypeOfTypes]]) + assert.deepStrictEqual( + midWild.match([Undefined, TypeOfTypes, TypeOfTypes]), + [3, [Undefined, TypeOfTypes, TypeOfTypes]]) + assert.deepStrictEqual( + midWild.match([Undefined, TypeOfTypes, Undefined]), + [-1, Undefined]) + assert.deepStrictEqual( + midWild.match([Undefined, TypeOfTypes]), + [-1, Undefined]) + assert.deepStrictEqual( + midWild.sampleTypes(), [Undefined, Undefined, TypeOfTypes]) + assert(midWild.equal(midWild)) + assert(!midWild.equal(pattern([Any, Any, TypeOfTypes]))) + }) + + it("supports greedy multiple and optional parameters", () => { + const midOpt = pattern([Undefined, Optional(TypeOfTypes), Undefined]) + assert.deepStrictEqual( + midOpt.match([Undefined, Undefined]), [2, [Undefined, [], Undefined]]) + assert.deepStrictEqual( + midOpt.match([Undefined, TypeOfTypes, Undefined]), + [3, [Undefined, [TypeOfTypes], Undefined]]) + assert.deepStrictEqual( + midOpt.match([Undefined, TypeOfTypes, TypeOfTypes]), + [-1, Undefined]) + assert.deepStrictEqual(midOpt.sampleTypes(), [Undefined, Undefined]) + const midMulti = pattern([Undefined, Multiple(TypeOfTypes), Undefined]) + assert.deepStrictEqual( + midMulti.match([Undefined, Undefined]), + [2, [Undefined, [], Undefined]]) + assert.deepStrictEqual( + midMulti.match([Undefined, TypeOfTypes, Undefined]), + [3, [Undefined, [TypeOfTypes], Undefined]]) + assert.deepStrictEqual( + midMulti.match( + [Undefined, TypeOfTypes, TypeOfTypes, TypeOfTypes, Undefined]), + [5, [Undefined, [TypeOfTypes, TypeOfTypes, TypeOfTypes], Undefined]]) + assert.deepStrictEqual( + midMulti.match([Undefined, TypeOfTypes, TypeOfTypes]), + [-1, Undefined]) + assert.deepStrictEqual(midOpt.sampleTypes(), [Undefined, Undefined]) + assert(!midMulti.equal(midOpt)) + const whyNot = pattern( + [Undefined, Multiple([Undefined, TypeOfTypes]), TypeOfTypes]) + assert.deepStrictEqual( + whyNot.match([Undefined, TypeOfTypes]), + [2, [Undefined, [], TypeOfTypes]]) + assert.deepStrictEqual( + whyNot.match([Undefined, Undefined, TypeOfTypes, TypeOfTypes]), + [4, [Undefined, [[Undefined, TypeOfTypes]], TypeOfTypes]]) + assert.deepStrictEqual( + whyNot.match([ + Undefined, + Undefined, TypeOfTypes, Undefined, TypeOfTypes, + TypeOfTypes + ]), + [6, [ + Undefined, + [[Undefined, TypeOfTypes], [Undefined, TypeOfTypes]], + TypeOfTypes + ]]) + assert.deepStrictEqual( + whyNot.match([Undefined, TypeOfTypes, Undefined, TypeOfTypes]), + [2, [Undefined, [], TypeOfTypes]]) + assert.deepStrictEqual( + whyNot.sampleTypes(), [Undefined, TypeOfTypes]) + assert(whyNot.equal(whyNot)) + }) +}) diff --git a/src/core/__test__/helpers.spec.js b/src/core/__test__/helpers.spec.js new file mode 100644 index 0000000..50969bc --- /dev/null +++ b/src/core/__test__/helpers.spec.js @@ -0,0 +1,35 @@ +import assert from 'assert' +import { + Implementations, onType, isPlainObject, isPlainFunction +} from '../helpers.js' +import {Type, Undefined, TypeOfTypes} from '../Type.js' +import {TypePattern} from '../TypePatterns.js' + +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)) + }) + + it('detects plain objects', () => { + assert(!isPlainObject(Undefined)) + assert(!isPlainObject([2, 3, 4])) + assert(!isPlainObject('pat')) + assert(isPlainObject({plain: 'object'})) + assert(isPlainObject({})) + }) + + it('detects plain functions (not constructors)', () => { + function embedded() {return 1} + class Embedded {} + assert(isPlainFunction(embedded)) + assert(isPlainFunction(() => 'wow')) + assert(isPlainFunction(it)) + assert(!isPlainFunction(7)) + assert(!isPlainFunction(Implementations)) + assert(!isPlainFunction(Type)) + assert(!isPlainFunction(Embedded)) + }) +}) diff --git a/src/core/helpers.js b/src/core/helpers.js index a703fef..7d12fe0 100644 --- a/src/core/helpers.js +++ b/src/core/helpers.js @@ -12,11 +12,35 @@ export class Implementations { export const onType = (...imps) => new Implementations(imps) -export const Returns = (type, f) => (f.returns = type, f) - -export const isPlainObject = (obj) => { +export const isPlainObject = obj => { if (typeof obj !== 'object') return false if (!obj) return false // excludes null const proto = Object.getPrototypeOf(obj) return !proto || proto === Object.prototype } + +// Class entities have typeof === 'function', but mostly we do not +// want to treat them like ordinary functions, hence we need some way +// of telling. There doesn't seem to be a definitive way, so we +// use a mishmash of techniques from +// https://stackoverflow.com/questions/526559 +const isClass = f => { + if (!f.prototype) return false + if (f.constructor !== Function) return false + const protoCtor = f.prototype.constructor + if (protoCtor + && protoCtor.toString + && protoCtor.toString().startsWith('class') + ) { + return true + } + if (Object.getPrototypeOf(f) !== Function.prototype) return true + if (Object.getOwnPropertyNames(f).includes('arguments')) return false + return Object.getOwnPropertyNames(f.prototype).length > 1 +} + +// returns true for functions that are not class constructors, at +// least so far as we can tell: +export const isPlainFunction = f => { + return typeof f === 'function' && !isClass(f) +} diff --git a/src/number/__test__/type.spec.js b/src/number/__test__/type.spec.js new file mode 100644 index 0000000..17cf656 --- /dev/null +++ b/src/number/__test__/type.spec.js @@ -0,0 +1,8 @@ +import assert from 'assert' +import math from '#nanomath' + +describe('number type operations', () => { + it('converts to number', () => { + assert.strictEqual(math.number(2.637), 2.637) + }) +}) diff --git a/src/number/__test__/utils.spec.js b/src/number/__test__/utils.spec.js new file mode 100644 index 0000000..50651b0 --- /dev/null +++ b/src/number/__test__/utils.spec.js @@ -0,0 +1,8 @@ +import assert from 'assert' +import math from '#nanomath' + +describe('number utilities', () => { + it('clones a number', () => { + assert.strictEqual(math.clone(2.637), 2.637) + }) +}) diff --git a/src/number/helpers.js b/src/number/helpers.js index 774fc6e..8222ed6 100644 --- a/src/number/helpers.js +++ b/src/number/helpers.js @@ -1,5 +1,7 @@ -import {Returns, onType} from '#core/helpers.js' import {Number} from './Number.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)) diff --git a/src/package.json b/src/package.json index 4cfdaef..41ce1cd 100644 --- a/src/package.json +++ b/src/package.json @@ -1,7 +1,8 @@ { "imports" : { "#nanomath": "./nanomath.js", - "#core/*.js": "./core/*.js" + "#core/*.js": "./core/*.js", + "#number/*.js": "./number/*.js" }, "type" : "module" }