feat: config and approximate equality #19
17 changed files with 181 additions and 53 deletions
|
@ -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
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
14
src/coretypes/__test__/utils.spec.js
Normal file
14
src/coretypes/__test__/utils.spec.js
Normal file
|
@ -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)
|
||||
})
|
||||
})
|
|
@ -1 +1,2 @@
|
|||
export * from './relational.js'
|
||||
export * from './utils.js'
|
||||
|
|
26
src/coretypes/utils.js
Normal file
26
src/coretypes/utils.js
Normal file
|
@ -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"`)
|
||||
})
|
||||
)
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
13
src/generic/__test__/utils.spec.js
Normal file
13
src/generic/__test__/utils.spec.js
Normal file
|
@ -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))
|
||||
})
|
||||
})
|
|
@ -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'
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
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]) => {
|
||||
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
|
||||
|
@ -34,3 +35,18 @@ export const equal = onType(
|
|||
// 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
|
||||
})
|
||||
})
|
||||
|
|
10
src/generic/utils.js
Normal file
10
src/generic/utils.js
Normal file
|
@ -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))
|
||||
}
|
|
@ -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
|
||||
})
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue