feat: config and approximate equality #19

Merged
glen merged 4 commits from config into main 2025-04-16 04:23:49 +00:00
17 changed files with 181 additions and 53 deletions
Showing only changes of commit 686cd93927 - Show all commits

View file

@ -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
})

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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)
})
}) })

View file

@ -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

View 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)
})
})

View file

@ -1 +1,2 @@
export * from './relational.js' export * from './relational.js'
export * from './utils.js'

26
src/coretypes/utils.js Normal file
View 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"`)
})
)

View file

@ -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)
})
}) })

View 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))
})
})

View file

@ -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'

View file

@ -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
View 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))
}

View file

@ -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
}) })

View file

@ -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', () => {

View file

@ -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)
) )