diff --git a/src/boolean/BooleanT.js b/src/boolean/BooleanT.js index 8d26974..f2792ed 100644 --- a/src/boolean/BooleanT.js +++ b/src/boolean/BooleanT.js @@ -1,3 +1,6 @@ import {Type} from '#core/Type.js' -export const BooleanT = new Type(n => typeof n === 'boolean') +export const BooleanT = new Type(n => typeof n === 'boolean', { + zero: false, + one: true +}) diff --git a/src/core/README.md b/src/core/README.md index 10d3401..0cdfd69 100644 --- a/src/core/README.md +++ b/src/core/README.md @@ -13,6 +13,13 @@ As of this writing, the only two types required to be in a TypeDispatcher are Undefined (the type inhabited only by `undefined`) and TypeOfTypes (the type inhabited exactly by Type objects). +There is also a constant NotAType which is the type-world analogue of NaN for +numbers. It is occasionally used for the rare behavior that truly does not +return any particular type, such as the method `zero` that takes a Type and +returns its zero element. However, it does not really work as a Type, and in +particular, do _not_ merge it into any TypeDispatcher -- it will disrupt the +type and method resolution process. + ## Core methods Similarly, as of this writing the only methods that must be in a TypeDispatcher diff --git a/src/core/Type.js b/src/core/Type.js index 7c728a2..d95e5b7 100644 --- a/src/core/Type.js +++ b/src/core/Type.js @@ -2,14 +2,20 @@ export class Type { constructor(f, options = {}) { this.test = f this.from = options.from ?? {patterns: []} // mock empty Implementations + if ('zero' in options) this.zero = options.zero + if ('one' in options) this.one = options.one } toString() { return this.name || `[Type ${this.test}]` } } -export const Undefined = new Type(t => typeof t === 'undefined') +export const Undefined = new Type( + t => typeof t === 'undefined', + {zero: undefined, one: undefined}) export const TypeOfTypes = new Type(t => t instanceof Type) +export const NotAType = new Type(t => true) // Danger, do not merge! +NotAType._doNotMerge = true export const Returns = (type, f) => (f.returns = type, f) diff --git a/src/core/TypeDispatcher.js b/src/core/TypeDispatcher.js index 503b577..fdcad33 100644 --- a/src/core/TypeDispatcher.js +++ b/src/core/TypeDispatcher.js @@ -1,7 +1,7 @@ import ArrayKeyedMap from 'array-keyed-map' import { - Implementations, ImplementationsGenerator, + Implementations, ImplementationsGenerator, ResolutionError, isPlainFunction, isPlainObject, onType, types } from './helpers.js' import {bootstrapTypes, Returns, whichType, Type} from './Type.js' @@ -80,6 +80,9 @@ export class TypeDispatcher { // Now dispatch on what sort of thing we are supposed to merge: if (val instanceof Type) { + if (val._doNotMerge) { + throw new TypeError(`attempt to merge unusable type '${val}'`) + } this.types[key] = val val.name = key continue @@ -219,22 +222,21 @@ export class TypeDispatcher { // transforms them per the given template, to massage them into the form // expected by a behavior associated with the TypePattern that produced // the template. - _generateCollectFunction(template, state = {pos: 0}) { - const extractors = [] - for (const elt of template) { - if (Array.isArray(elt)) { - extractors.push(this._generateCollectFunction(elt, state)) - } else { - const from = state.pos++ - if ('actual' in elt) { // incorporate conversion - let convert = elt.convertor - if (!convert.returns) { // it's a factory that produces convert - convert = convert(this, elt.actual) - } - extractors.push(args => convert(args[from])) - } else extractors.push(args => args[from]) + _generateCollectFunction(template, state=false) { + if (!Array.isArray(template)) { + const from = state ? state.pos++ : 0 + let extractor = args => args[from] + if ('actual' in template) { // incorporate conversion + let convert = template.convertor + // Check if it's a factory: + if (!convert.returns) convert = convert(this, template.actual) + extractor = args => convert(args[from]) } + return state ? extractor : args => [extractor(args)] } + state ||= {pos: 0} + const extractors = template.map( + item => this._generateCollectFunction(item, state)) return args => extractors.map(f => f(args)) } @@ -284,7 +286,8 @@ export class TypeDispatcher { template = types } if (needItem) { - throw new TypeError(`no matching definition of '${key}' on '${types}'`) + throw new ResolutionError( + `no matching definition of '${key}' on '${types}'`) } // If this key is producing a non-function value, we're done if (!isPlainFunction(item)) { @@ -316,7 +319,7 @@ export class TypeDispatcher { matched(template)) } catch (e) { e.message = `Error in factory for ${key} on ${types} ` - + `(match data ${template}): ${e}` + + `(match data ${template}): ${e.message}` throw e } } else theBehavior = item diff --git a/src/core/__test__/TypeDispatcher.spec.js b/src/core/__test__/TypeDispatcher.spec.js index 81d91a3..25ed721 100644 --- a/src/core/__test__/TypeDispatcher.spec.js +++ b/src/core/__test__/TypeDispatcher.spec.js @@ -3,9 +3,9 @@ import {TypeDispatcher} from '../TypeDispatcher.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 {onType, ResolutionError} from "#core/helpers.js" import {Any} from "#core/TypePatterns.js" -import {Returns} from "#core/Type.js" +import {Returns, NotAType} from "#core/Type.js" import {plain} from "#number/helpers.js" describe('TypeDispatcher', () => { @@ -17,6 +17,7 @@ describe('TypeDispatcher', () => { const {NumberT, TypeOfTypes, Undefined} = incremental.types assert(NumberT.test(7)) assert.strictEqual(incremental.add(-1.5, 0.5), -1) + assert.throws(() => incremental.add(7, undefined), ResolutionError) // Make Undefined act like zero: incremental.merge({add: onType( [Undefined, Any], (_m, [_U, T]) => Returns(T, (_a, b) => b), @@ -30,7 +31,7 @@ describe('TypeDispatcher', () => { assert.strictEqual( incremental.add.resolve([Undefined, NumberT]).returns, NumberT) - // Oops, changed my mind, make it work like NaN with numbers: + // Oops, changed my mind ;-), make it work like NaN with numbers: const alwaysNaN = Returns(NumberT, () => NaN) incremental.merge({add: onType( [Undefined, NumberT], alwaysNaN, @@ -63,4 +64,8 @@ describe('TypeDispatcher', () => { assert(!bgn._behaviors.negate.has([BooleanT])) assert.strictEqual(bgn.negate(true), -2) }) + it('disallows merging NotAType', () => { + const doomed = new TypeDispatcher() + assert.throws(() => doomed.merge({NaT: NotAType}), TypeError) + }) }) diff --git a/src/core/helpers.js b/src/core/helpers.js index 8ecded5..f741859 100644 --- a/src/core/helpers.js +++ b/src/core/helpers.js @@ -36,6 +36,13 @@ export class ImplementationsGenerator { export const types = new ImplementationsGenerator(() => onType(Passthru, {})) +export class ResolutionError extends TypeError { + constructor(...args) { + super(...args) + this.name = 'ResolutionError' + } +} + export const isPlainObject = obj => { if (typeof obj !== 'object') return false if (!obj) return false // excludes null diff --git a/src/coretypes/__test__/utils.spec.js b/src/coretypes/__test__/utils.spec.js new file mode 100644 index 0000000..732bf1f --- /dev/null +++ b/src/coretypes/__test__/utils.spec.js @@ -0,0 +1,14 @@ +import assert from 'assert' +import math from '#nanomath' + +describe('core type utility functions', () => { + it('identifies zero and one elements in most types', () => { + assert.strictEqual(math.zero(math.types.NumberT), 0) + assert.strictEqual(math.zero(math.types.Undefined), undefined) + assert.strictEqual(math.one(math.types.BooleanT), true) + assert.throws(() => math.one(math.types.TypeOfTypes), RangeError) + assert.strictEqual(math.one(-7.5), 1) + assert.strictEqual(math.one(undefined), undefined) + assert.strictEqual(math.zero(true), false) + }) +}) diff --git a/src/coretypes/all.js b/src/coretypes/all.js index ff64f94..18b23af 100644 --- a/src/coretypes/all.js +++ b/src/coretypes/all.js @@ -1 +1,2 @@ export * from './relational.js' +export * from './utils.js' diff --git a/src/coretypes/utils.js b/src/coretypes/utils.js new file mode 100644 index 0000000..6bc35f0 --- /dev/null +++ b/src/coretypes/utils.js @@ -0,0 +1,26 @@ +import {onType} from '#core/helpers.js' +import {NotAType, Returns, TypeOfTypes} from '#core/Type.js' +import {Any} from "#core/TypePatterns.js" + +export const zero = onType( + Any, (math, T) => { + const z = math.zero(T) + return Returns(T, () => z) + }, + TypeOfTypes, Returns(NotAType, t => { + if ('zero' in t) return t.zero + throw new RangeError(`type '${t}' has no zero element`) + }) +) + +export const one = onType( + Any, (math, T) => { + const unit = math.one(T) + return Returns(T, () => unit) + }, + TypeOfTypes, Returns(NotAType, t => { + if ('one' in t) return t.one + throw new RangeError( + `type '${t}' has no unit element designated as "one"`) + }) +) diff --git a/src/generic/__test__/relational.spec.js b/src/generic/__test__/relational.spec.js index 5d61b76..6506f6a 100644 --- a/src/generic/__test__/relational.spec.js +++ b/src/generic/__test__/relational.spec.js @@ -2,6 +2,7 @@ import assert from 'assert' import math from '#nanomath' import * as numbers from '#number/all.js' import * as generics from '#generic/all.js' +import {ResolutionError} from '#core/helpers.js' import {TypeDispatcher} from '#core/TypeDispatcher.js' describe('generic relational functions', () => { @@ -38,4 +39,15 @@ describe('generic relational functions', () => { assert.strictEqual(equal(0, 1.1e-15), 1) assert.strictEqual(equal(0, 1.1e-13), 0) }) + it('performs three-way comparison', () => { + const {compare} = math + assert.strictEqual(compare(-0.4e-15, +0.4e-15), 0) + assert.strictEqual(compare(2.2, true), 1) + assert.strictEqual(compare(-Infinity, 7), -1) + assert(isNaN(compare(NaN, 0))) + assert(isNaN(compare(false, NaN))) + assert(isNaN(compare(NaN, NaN))) + assert.strictEqual(compare(true, false), 1) + assert.throws(() => compare(undefined, -1), ResolutionError) + }) }) diff --git a/src/generic/__test__/utils.spec.js b/src/generic/__test__/utils.spec.js new file mode 100644 index 0000000..e178a2d --- /dev/null +++ b/src/generic/__test__/utils.spec.js @@ -0,0 +1,13 @@ +import assert from 'assert' +import math from '#nanomath' + +describe('generic utility functions', () => { + it('tests whether an element is zero', () => { + const {isZero} = math + assert(!isZero(3)) + assert(isZero(3e-16)) + assert(isZero(false)) + assert(!isZero(true)) + assert(isZero(undefined)) + }) +}) diff --git a/src/generic/all.js b/src/generic/all.js index 951d1f6..4dea4d2 100644 --- a/src/generic/all.js +++ b/src/generic/all.js @@ -1,3 +1,4 @@ export * as arithmetic from './arithmetic.js' export * as configuration from './config.js' export * as relational from './relational.js' +export * as utilities from './utils.js' diff --git a/src/generic/relational.js b/src/generic/relational.js index 7e7f92f..acade73 100644 --- a/src/generic/relational.js +++ b/src/generic/relational.js @@ -1,36 +1,52 @@ import {ReturnsAs} from './helpers.js' import {onType} from '#core/helpers.js' +import {Returns} from '#core/Type.js' import {Any, matched} from '#core/TypePatterns.js' import {boolnum} from '#number/helpers.js' +import {NumberT} from '#number/NumberT.js' -export const equal = onType( - [Any, Any], (math, [T, U]) => { - // Finding the correct signature of `indistinguishable` to use for - // testing (approximate) equality is tricky, because T or U might - // need to be converted for the sake of comparison, and some types - // allow tolerances for equality and others don't. So the plan is - // we first look up without tolerances, then we check the config for - // the matching type, and then we look up with tolerances. - let exactChecker +export const equal = onType([Any, Any], (math, [T, U]) => { + // Finding the correct signature of `indistinguishable` to use for + // testing (approximate) equality is tricky, because T or U might + // need to be converted for the sake of comparison, and some types + // allow tolerances for equality and others don't. So the plan is + // we first look up without tolerances, then we check the config for + // the matching type, and then we look up with tolerances. + let exactChecker + try { + exactChecker = math.indistinguishable.resolve([T, U]) + } catch { // can't compare, so no way they can be equal + return boolnum(() => false)(math) + } + // Get the type of the first argument to the matching checker: + const ByType = matched(exactChecker.template).flat()[0] + // Now see if there are tolerances for that type: + const typeConfig = math.resolve('config', [ByType]) + if ('relTol' in typeConfig) { try { - exactChecker = math.indistinguishable.resolve([T, U]) - } catch { // can't compare, so no way they can be equal - return boolnum(() => false)(math) - } - // Get the type of the first argument to the matching checker: - const ByType = matched(exactChecker.template).flat()[0] - // Now see if there are tolerances for that type: - const typeConfig = math.resolve('config', [ByType]) - if ('relTol' in typeConfig) { - try { - const {relTol, absTol} = typeConfig - const RT = math.typeOf(relTol) - const AT = math.typeOf(absTol) - const approx = math.indistinguishable.resolve([T, U, RT, AT]) - return ReturnsAs( - approx, (t, u) => approx(t, u, relTol, absTol)) - } catch {} // fall through to case with no tolerances - } - // either no tolerances or no matching signature for indistinguishable - return exactChecker - }) + const {relTol, absTol} = typeConfig + const RT = math.typeOf(relTol) + const AT = math.typeOf(absTol) + const approx = math.indistinguishable.resolve([T, U, RT, AT]) + return ReturnsAs( + approx, (t, u) => approx(t, u, relTol, absTol)) + } catch {} // fall through to case with no tolerances + } + // either no tolerances or no matching signature for indistinguishable + return exactChecker +}) + +// now that we have `equal` and `exceeds`, pretty much everything else should +// be easy: + +export const compare = onType([Any, Any], (math, [T, U]) => { + const eq = math.equal.resolve([T, U]) + const gt = math.exceeds.resolve([T, U]) + const isTnan = math.isnan.resolve([T]) + const isUnan = math.isnan.resolve([U]) + return Returns(NumberT, (t, u) => { + if (isTnan(t) || isUnan(u)) return NaN + if (eq(t,u)) return 0 + return gt(t, u) ? 1 : -1 + }) +}) diff --git a/src/generic/utils.js b/src/generic/utils.js new file mode 100644 index 0000000..fc78903 --- /dev/null +++ b/src/generic/utils.js @@ -0,0 +1,10 @@ +import {ReturnsAs} from './helpers.js' +import {onType} from '#core/helpers.js' +import {Returns} from '#core/Type.js' +import {Any} from "#core/TypePatterns.js" + +export const isZero = (math, [T]) => { + const z = math.zero(T) + const eq = math.equal.resolve([T, T]) + return ReturnsAs(eq, x => eq(z, x)) +} diff --git a/src/number/NumberT.js b/src/number/NumberT.js index 99c53ff..332fab6 100644 --- a/src/number/NumberT.js +++ b/src/number/NumberT.js @@ -4,4 +4,6 @@ import {BooleanT} from '#boolean/BooleanT.js' export const NumberT = new Type(n => typeof n === 'number', { from: onType(BooleanT, math => math.number.resolve([BooleanT])), + one: 1, + zero: 0 }) diff --git a/src/number/__test__/relational.spec.js b/src/number/__test__/relational.spec.js index 9908482..ce5d8eb 100644 --- a/src/number/__test__/relational.spec.js +++ b/src/number/__test__/relational.spec.js @@ -12,6 +12,7 @@ describe('number relational functions', () => { assert(exceeds(-1e101, -Infinity)) assert(!exceeds(NaN, 0)) assert(!exceeds(0, NaN)) + assert(!exceeds(NaN, NaN)) assert(!exceeds(2, 2)) }) it('checks for exact equality', () => { diff --git a/src/number/type.js b/src/number/type.js index 3a9ebfc..9ec8fed 100644 --- a/src/number/type.js +++ b/src/number/type.js @@ -7,6 +7,7 @@ const num = f => Returns(NumberT, f) export const number = plain(a => a) number.also( - BooleanT, num(p => p ? 1 : 0), + // conversions from Boolean should be consistent with one and zero: + BooleanT, num(p => p ? NumberT.one : NumberT.zero), [], num(() => 0) )