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'
|
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
|
Undefined (the type inhabited only by `undefined`) and TypeOfTypes (the type
|
||||||
inhabited exactly by Type objects).
|
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
|
## Core methods
|
||||||
|
|
||||||
Similarly, as of this writing the only methods that must be in a TypeDispatcher
|
Similarly, as of this writing the only methods that must be in a TypeDispatcher
|
||||||
|
|
|
@ -2,14 +2,20 @@ export class Type {
|
||||||
constructor(f, options = {}) {
|
constructor(f, options = {}) {
|
||||||
this.test = f
|
this.test = f
|
||||||
this.from = options.from ?? {patterns: []} // mock empty Implementations
|
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() {
|
toString() {
|
||||||
return this.name || `[Type ${this.test}]`
|
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 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)
|
export const Returns = (type, f) => (f.returns = type, f)
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import ArrayKeyedMap from 'array-keyed-map'
|
import ArrayKeyedMap from 'array-keyed-map'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Implementations, ImplementationsGenerator,
|
Implementations, ImplementationsGenerator, ResolutionError,
|
||||||
isPlainFunction, isPlainObject, onType, types
|
isPlainFunction, isPlainObject, onType, types
|
||||||
} from './helpers.js'
|
} from './helpers.js'
|
||||||
import {bootstrapTypes, Returns, whichType, Type} from './Type.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:
|
// Now dispatch on what sort of thing we are supposed to merge:
|
||||||
if (val instanceof Type) {
|
if (val instanceof Type) {
|
||||||
|
if (val._doNotMerge) {
|
||||||
|
throw new TypeError(`attempt to merge unusable type '${val}'`)
|
||||||
|
}
|
||||||
this.types[key] = val
|
this.types[key] = val
|
||||||
val.name = key
|
val.name = key
|
||||||
continue
|
continue
|
||||||
|
@ -219,22 +222,21 @@ export class TypeDispatcher {
|
||||||
// transforms them per the given template, to massage them into the form
|
// transforms them per the given template, to massage them into the form
|
||||||
// expected by a behavior associated with the TypePattern that produced
|
// expected by a behavior associated with the TypePattern that produced
|
||||||
// the template.
|
// the template.
|
||||||
_generateCollectFunction(template, state = {pos: 0}) {
|
_generateCollectFunction(template, state=false) {
|
||||||
const extractors = []
|
if (!Array.isArray(template)) {
|
||||||
for (const elt of template) {
|
const from = state ? state.pos++ : 0
|
||||||
if (Array.isArray(elt)) {
|
let extractor = args => args[from]
|
||||||
extractors.push(this._generateCollectFunction(elt, state))
|
if ('actual' in template) { // incorporate conversion
|
||||||
} else {
|
let convert = template.convertor
|
||||||
const from = state.pos++
|
// Check if it's a factory:
|
||||||
if ('actual' in elt) { // incorporate conversion
|
if (!convert.returns) convert = convert(this, template.actual)
|
||||||
let convert = elt.convertor
|
extractor = args => convert(args[from])
|
||||||
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])
|
|
||||||
}
|
}
|
||||||
|
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))
|
return args => extractors.map(f => f(args))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -284,7 +286,8 @@ export class TypeDispatcher {
|
||||||
template = types
|
template = types
|
||||||
}
|
}
|
||||||
if (needItem) {
|
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 this key is producing a non-function value, we're done
|
||||||
if (!isPlainFunction(item)) {
|
if (!isPlainFunction(item)) {
|
||||||
|
@ -316,7 +319,7 @@ export class TypeDispatcher {
|
||||||
matched(template))
|
matched(template))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
e.message = `Error in factory for ${key} on ${types} `
|
e.message = `Error in factory for ${key} on ${types} `
|
||||||
+ `(match data ${template}): ${e}`
|
+ `(match data ${template}): ${e.message}`
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
} else theBehavior = item
|
} else theBehavior = item
|
||||||
|
|
|
@ -3,9 +3,9 @@ import {TypeDispatcher} from '../TypeDispatcher.js'
|
||||||
import * as booleans from '#boolean/all.js'
|
import * as booleans from '#boolean/all.js'
|
||||||
import * as generics from '#generic/all.js'
|
import * as generics from '#generic/all.js'
|
||||||
import * as numbers from '#number/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 {Any} from "#core/TypePatterns.js"
|
||||||
import {Returns} from "#core/Type.js"
|
import {Returns, NotAType} from "#core/Type.js"
|
||||||
import {plain} from "#number/helpers.js"
|
import {plain} from "#number/helpers.js"
|
||||||
|
|
||||||
describe('TypeDispatcher', () => {
|
describe('TypeDispatcher', () => {
|
||||||
|
@ -17,6 +17,7 @@ describe('TypeDispatcher', () => {
|
||||||
const {NumberT, TypeOfTypes, Undefined} = incremental.types
|
const {NumberT, TypeOfTypes, Undefined} = incremental.types
|
||||||
assert(NumberT.test(7))
|
assert(NumberT.test(7))
|
||||||
assert.strictEqual(incremental.add(-1.5, 0.5), -1)
|
assert.strictEqual(incremental.add(-1.5, 0.5), -1)
|
||||||
|
assert.throws(() => incremental.add(7, undefined), ResolutionError)
|
||||||
// Make Undefined act like zero:
|
// Make Undefined act like zero:
|
||||||
incremental.merge({add: onType(
|
incremental.merge({add: onType(
|
||||||
[Undefined, Any], (_m, [_U, T]) => Returns(T, (_a, b) => b),
|
[Undefined, Any], (_m, [_U, T]) => Returns(T, (_a, b) => b),
|
||||||
|
@ -30,7 +31,7 @@ describe('TypeDispatcher', () => {
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
incremental.add.resolve([Undefined, NumberT]).returns,
|
incremental.add.resolve([Undefined, NumberT]).returns,
|
||||||
NumberT)
|
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)
|
const alwaysNaN = Returns(NumberT, () => NaN)
|
||||||
incremental.merge({add: onType(
|
incremental.merge({add: onType(
|
||||||
[Undefined, NumberT], alwaysNaN,
|
[Undefined, NumberT], alwaysNaN,
|
||||||
|
@ -63,4 +64,8 @@ describe('TypeDispatcher', () => {
|
||||||
assert(!bgn._behaviors.negate.has([BooleanT]))
|
assert(!bgn._behaviors.negate.has([BooleanT]))
|
||||||
assert.strictEqual(bgn.negate(true), -2)
|
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 const types = new ImplementationsGenerator(() => onType(Passthru, {}))
|
||||||
|
|
||||||
|
export class ResolutionError extends TypeError {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args)
|
||||||
|
this.name = 'ResolutionError'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const isPlainObject = obj => {
|
export const isPlainObject = obj => {
|
||||||
if (typeof obj !== 'object') return false
|
if (typeof obj !== 'object') return false
|
||||||
if (!obj) return false // excludes null
|
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 './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 math from '#nanomath'
|
||||||
import * as numbers from '#number/all.js'
|
import * as numbers from '#number/all.js'
|
||||||
import * as generics from '#generic/all.js'
|
import * as generics from '#generic/all.js'
|
||||||
|
import {ResolutionError} from '#core/helpers.js'
|
||||||
import {TypeDispatcher} from '#core/TypeDispatcher.js'
|
import {TypeDispatcher} from '#core/TypeDispatcher.js'
|
||||||
|
|
||||||
describe('generic relational functions', () => {
|
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-15), 1)
|
||||||
assert.strictEqual(equal(0, 1.1e-13), 0)
|
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 arithmetic from './arithmetic.js'
|
||||||
export * as configuration from './config.js'
|
export * as configuration from './config.js'
|
||||||
export * as relational from './relational.js'
|
export * as relational from './relational.js'
|
||||||
|
export * as utilities from './utils.js'
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import {ReturnsAs} from './helpers.js'
|
import {ReturnsAs} from './helpers.js'
|
||||||
import {onType} from '#core/helpers.js'
|
import {onType} from '#core/helpers.js'
|
||||||
|
import {Returns} from '#core/Type.js'
|
||||||
import {Any, matched} from '#core/TypePatterns.js'
|
import {Any, matched} from '#core/TypePatterns.js'
|
||||||
import {boolnum} from '#number/helpers.js'
|
import {boolnum} from '#number/helpers.js'
|
||||||
|
import {NumberT} from '#number/NumberT.js'
|
||||||
|
|
||||||
export const equal = onType(
|
export const equal = onType([Any, Any], (math, [T, U]) => {
|
||||||
[Any, Any], (math, [T, U]) => {
|
|
||||||
// Finding the correct signature of `indistinguishable` to use for
|
// Finding the correct signature of `indistinguishable` to use for
|
||||||
// testing (approximate) equality is tricky, because T or U might
|
// testing (approximate) equality is tricky, because T or U might
|
||||||
// need to be converted for the sake of comparison, and some types
|
// need to be converted for the sake of comparison, and some types
|
||||||
|
@ -33,4 +34,19 @@ export const equal = onType(
|
||||||
}
|
}
|
||||||
// either no tolerances or no matching signature for indistinguishable
|
// either no tolerances or no matching signature for indistinguishable
|
||||||
return exactChecker
|
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', {
|
export const NumberT = new Type(n => typeof n === 'number', {
|
||||||
from: onType(BooleanT, math => math.number.resolve([BooleanT])),
|
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(-1e101, -Infinity))
|
||||||
assert(!exceeds(NaN, 0))
|
assert(!exceeds(NaN, 0))
|
||||||
assert(!exceeds(0, NaN))
|
assert(!exceeds(0, NaN))
|
||||||
|
assert(!exceeds(NaN, NaN))
|
||||||
assert(!exceeds(2, 2))
|
assert(!exceeds(2, 2))
|
||||||
})
|
})
|
||||||
it('checks for exact equality', () => {
|
it('checks for exact equality', () => {
|
||||||
|
|
|
@ -7,6 +7,7 @@ const num = f => Returns(NumberT, f)
|
||||||
|
|
||||||
export const number = plain(a => a)
|
export const number = plain(a => a)
|
||||||
number.also(
|
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)
|
[], num(() => 0)
|
||||||
)
|
)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue