refactor: change onType to match and take only one pattern and result
All checks were successful
/ test (pull_request) Successful in 17s

This commit is contained in:
Glen Whitney 2025-04-21 21:54:49 -07:00
parent 491e207fad
commit 92e232e06d
22 changed files with 147 additions and 135 deletions

View file

@ -1,14 +1,14 @@
import {BooleanT} from './BooleanT.js'
import {onType} from '#core/helpers.js'
import {match} from '#core/helpers.js'
import {Returns, Type, TypeOfTypes, Undefined} from '#core/Type.js'
import {NumberT} from '#number/NumberT.js'
const bool = f => Returns(BooleanT, f)
export const boolean = onType(
BooleanT, bool(p => p),
NumberT, bool(a => !!a),
TypeOfTypes, bool(() => true),
Undefined, bool(() => false),
[], bool(() => false)
)
export const boolean = [
match(BooleanT, bool(p => p)),
match(NumberT, bool(a => !!a)),
match(TypeOfTypes, bool(() => true)),
match(Undefined, bool(() => false)),
match([], bool(() => false))
]

View file

@ -1,5 +1,5 @@
import {Type} from '#core/Type.js'
import {onType} from '#core/helpers.js'
import {match} from '#core/helpers.js'
const isComplex = z => z && typeof z === 'object' && 're' in z && 'im' in z
@ -10,15 +10,17 @@ function complexSpecialize(ComponentType) {
const specTest = z => isComplex(z) && compTest(z.re) && compTest(z.im)
const typeName = `Complex(${ComponentType})`
if (ComponentType.concrete) {
const fromSpec = [
ComponentType, math => r => ({re: r, im: ComponentType.zero})]
for (const [matchType, fctry] of ComponentType.from.patterns) {
fromSpec.push(this.specialize(matchType.type), math => {
const compConv = fctry(math)
return z => ({re: compConv(z.re), im: compConv(z.im)})
})
const fromSpec = [match(
ComponentType, math => r => ({re: r, im: ComponentType.zero}))]
for (const {pattern, does} of ComponentType.from) {
fromSpec.push(match(
this.specialize(pattern.type),
math => {
const compConv = does(math)
return z => ({re: compConv(z.re), im: compConv(z.im)})
}))
}
const typeOptions = {from: onType(...fromSpec), typeName}
const typeOptions = {from: fromSpec, typeName}
if ('zero' in ComponentType) {
typeOptions.zero = {re: ComponentType.zero, im: ComponentType.zero}
if ('one' in ComponentType) {

View file

@ -21,11 +21,11 @@ describe('Complex Type', () => {
const convertImps = CplxNum.from
let cnvNtoCN
let cnvCBtoCN
for (const [pattern, convFactory] of convertImps.patterns) {
for (const {pattern, does} of convertImps) {
if (pattern.match([NumberT])[0] === 1) {
cnvNtoCN = convFactory(math)
cnvNtoCN = does(math)
} else if (pattern.match([Complex(BooleanT)])[0] === 1) {
cnvCBtoCN = convFactory(math)
cnvCBtoCN = does(math)
}
}
assert.deepStrictEqual(cnvNtoCN(3.5), {re: 3.5, im: 0})

View file

@ -1,10 +1,10 @@
import {Complex} from './Complex.js'
import {onType} from "#core/helpers.js"
import {match} from "#core/helpers.js"
import {Returns} from "#core/Type.js"
import {Any} from "#core/TypePatterns.js"
export const complex = onType(
Any, (math, T) => {
export const complex = [
match(Any, (math, T) => {
const z = math.zero(T)
if (math.hasnan(T)) {
const isnan = math.isnan.resolve([T])
@ -15,8 +15,8 @@ export const complex = onType(
})
}
return Returns(Complex(T), r => ({re: r, im: z}))
},
[Any, Any], (math, [T, U]) => {
}),
match([Any, Any], (math, [T, U]) => {
if (T !== U) {
throw new RangeError(
'mixed complex types disallowed '
@ -24,5 +24,4 @@ export const complex = onType(
}
return Returns(Complex(T), (r, m) => ({re: r, im: m}))
})
]

View file

@ -21,7 +21,10 @@ export class Type extends Function {
})
this.test = f
this.from = options.from ?? {patterns: []} // mock empty Implementations
// we want property `from` to end up as an array of Matchers:
this.from = options.from
? Array.isArray(options.from) ? options.from : [options.from]
: []
if ('zero' in options) this.zero = options.zero
if ('one' in options) this.one = options.one
if ('nan' in options) this.nan = options.nan

View file

@ -1,8 +1,8 @@
import ArrayKeyedMap from 'array-keyed-map'
import {
Implementations, ImplementationsGenerator, ResolutionError,
isPlainFunction, isPlainObject, onType, types
Implementations, ImplementationsGenerator, Matcher, ResolutionError,
isPlainFunction, isPlainObject, match, types
} from './helpers.js'
import {bootstrapTypes, Returns, whichType, Type} from './Type.js'
import {matched, needsCollection, Passthru} from './TypePatterns.js'
@ -45,11 +45,14 @@ export class TypeDispatcher {
// an Implementations that associates it with the Passthru pattern
// -- a factory for entities, to be invoked to get the value of a key
// for any types. You can just merge that.
// -- a value or behavior that applies just when the types of an
// argument match a type pattern. For that, you merge the result
// of `match(PATTERN, VALUE)`
// -- a collection of different values, or different behaviors, or
// different factories for different types for a given key. For that
// you merge an Implementations object that associates each item with
// a TypePattern. An Implementation object can most easily be
// generated with `onType(PATTERN, VALUE, PATTERN, VALUE,...)`
// different factories for different lists of argument types for a
// given key. For that you merge an array of results of `match`,
// so something like
// `[match(PATTERN, VALUE), match(PATTERN, VALUE), ...]`
// Initially I thought those were all the possibilities. But then I
// wanted to export something that when merged, would set the Passthru
// pattern to a fresh specific object for that merge, but so that the
@ -57,10 +60,10 @@ export class TypeDispatcher {
// particular TypeDispatcher (this situation applies to the config object).
// To produce that behavior, you need a fourth thing
// -- an ImplementationGenerator, which is basically a function that
// returns an Implementations object as above. As this is only needed
// for a single entity that will be merged into multiple different
// TypeDispatchers, there's not a big focus on making this convenient;
// it's not expected to come up much.
// returns one of the last couple of options above. As this variant is
// only needed for a single entity that will be merged into multiple
// different TypeDispatchers, there's not a big focus on making
// specifying it convenient; it's not expected to come up much.
merge(spec) {
if (!spec) return
@ -74,9 +77,7 @@ export class TypeDispatcher {
// For special cases like types, config, etc, we can wrap
// a function in ImplementationsGenerator to produce the thing
// we should really merge:
if (val instanceof ImplementationsGenerator) {
val = val.generate()
}
if (val instanceof ImplementationsGenerator) val = val.generate()
// Now dispatch on what sort of thing we are supposed to merge:
if (val instanceof Type) {
@ -100,7 +101,14 @@ export class TypeDispatcher {
// Everything else we coerce into Implementations and deal with
// right here:
if (!(val instanceof Implementations)) val = onType(Passthru, val)
if (val instanceof Matcher) val = new Implementations(val)
if (Array.isArray(val)) val = new Implementations(val)
if (!(val instanceof Implementations)) {
val = new Implementations(match(Passthru, val))
}
// hereafter we may assume that val is an instance of Implementations
if (!(key in this)) {
// Need to "bootstrap" the item:
// We initially define it with a temporary getter, only
@ -189,11 +197,11 @@ export class TypeDispatcher {
this._behaviors[key] = new ArrayKeyedMap()
}
// Now add all of the patterns of this implementation:
for (const [pattern, result] of val.patterns) {
// Now add all of the matchers of this Implementations:
for (const {pattern, does} of val.matchers) {
if (pattern === Passthru) {
if (key in this._fallbacks) this._disengageFallback(key)
this._fallbacks[key] = result
this._fallbacks[key] = does
} else {
this._clearBehaviorsMatching(key, pattern)
// if it happens the same pattern is already in the
@ -201,7 +209,7 @@ export class TypeDispatcher {
const imps = this._implementations[key]
const have = imps.findIndex(elt => pattern.equal(elt[0]))
if (have >= 0) imps.splice(have, 1)
this._implementations[key].unshift([pattern, result])
this._implementations[key].unshift([pattern, does])
}
}
}

View file

@ -21,7 +21,7 @@ class MatchTypePattern extends TypePattern {
if (position < typeSequence.length) {
if (this.type.specializesTo(actual)) return [position + 1, actual]
if (options.convert) {
for (const [pattern, convertor] of this.type.from.patterns) {
for (const {pattern, does: convertor} of this.type.from) {
const [pos] = pattern.match([actual])
if (pos === 1) {
return [position + 1, {actual, convertor, matched: this.type}]

View file

@ -4,7 +4,7 @@ import * as booleans from '#boolean/all.js'
import * as generics from '#generic/all.js'
import * as numbers from '#number/all.js'
import {NumberT} from '#number/NumberT.js'
import {onType, ResolutionError} from "#core/helpers.js"
import {match, ResolutionError} from "#core/helpers.js"
import {Any} from "#core/TypePatterns.js"
import {Returns, NotAType} from "#core/Type.js"
import {plain} from "#number/helpers.js"
@ -20,10 +20,10 @@ describe('TypeDispatcher', () => {
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),
[Any, Undefined], (_m, [T]) => Returns(T, a => a)
)})
incremental.merge({add: [
match([Undefined, Any], (_m, [_U, T]) => Returns(T, (_a, b) => b)),
match([Any, Undefined], (_m, [T]) => Returns(T, a => a))
]})
assert.strictEqual(incremental.add(7, undefined), 7)
assert.strictEqual(
incremental.resolve('add', [TypeOfTypes, Undefined]).returns,
@ -34,10 +34,10 @@ describe('TypeDispatcher', () => {
NumberT)
// Oops, changed my mind ;-), make it work like NaN with numbers:
const alwaysNaN = Returns(NumberT, () => NaN)
incremental.merge({add: onType(
[Undefined, NumberT], alwaysNaN,
[NumberT, Undefined], alwaysNaN
)})
incremental.merge({add: [
match([Undefined, NumberT], alwaysNaN),
match([NumberT, Undefined], alwaysNaN)
]})
assert(isNaN(incremental.add(undefined, -3.25)))
assert.strictEqual(
incremental.add.resolve([Undefined, NumberT]).returns,
@ -61,7 +61,7 @@ describe('TypeDispatcher', () => {
assert.strictEqual(bgn.negate(true), -1)
assert(bgn._behaviors.negate.has([BooleanT]))
const deps = bgn._dependencies.negate
bgn.merge({number: onType([BooleanT], Returns(NumberT, b => b ? 2 : 0))})
bgn.merge({number: match([BooleanT], Returns(NumberT, b => b ? 2 : 0))})
assert(!bgn._behaviors.negate.has([BooleanT]))
assert.strictEqual(bgn.negate(true), -2)
})

View file

@ -1,16 +1,15 @@
import assert from 'assert'
import {
Implementations, onType, isPlainObject, isPlainFunction
Matcher, match, isPlainObject, isPlainFunction, Implementations
} 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 Array)
assert(imps.patterns.every(([k]) => k instanceof TypePattern))
it('defines what Matchers are', () => {
const matcher = match([TypeOfTypes, Undefined], -3)
assert(matcher instanceof Matcher)
assert(matcher.pattern instanceof TypePattern)
})
it('detects plain objects', () => {

View file

@ -1,21 +1,21 @@
import {pattern, Passthru} from './TypePatterns.js'
export class Implementations {
constructor(imps) {
this.patterns = []
this._add(imps)
}
_add(imps) {
for (let i = 0; i < imps.length; ++i) {
this.patterns.push([pattern(imps[i]), imps[++i]])
}
}
also(...imps) {
this._add(imps)
export class Matcher {
constructor(spec, facOrBehave) {
this.pattern = pattern(spec)
this.does = facOrBehave
}
}
export const onType = (...imps) => new Implementations(imps)
export class Implementations {
constructor(impOrImps) {
if (Array.isArray(impOrImps)) {
this.matchers = impOrImps
} else this.matchers = [impOrImps]
}
}
export const match = (spec, facOrBehave) => new Matcher(spec, facOrBehave)
export class ImplementationsGenerator {
constructor(f) {
@ -34,7 +34,7 @@ export class ImplementationsGenerator {
// collection of types (and modifying the types in one would affect them
// all). Hence we do:
export const types = new ImplementationsGenerator(() => onType(Passthru, {}))
export const types = new ImplementationsGenerator(() => match(Passthru, {}))
export class ResolutionError extends TypeError {
constructor(...args) {

View file

@ -1,8 +1,8 @@
import {onType} from '#core/helpers.js'
import {match} from '#core/helpers.js'
import {TypeOfTypes, Undefined} from '#core/Type.js'
import {boolnum} from '#number/helpers.js'
export const indistinguishable = onType(
[Undefined, Undefined], boolnum(() => true),
[TypeOfTypes, TypeOfTypes], boolnum((t, u) => t === u)
)
export const indistinguishable = [
match([Undefined, Undefined], boolnum(() => true)),
match([TypeOfTypes, TypeOfTypes], boolnum((t, u) => t === u))
]

View file

@ -1,47 +1,47 @@
import {onType} from '#core/helpers.js'
import {match} from '#core/helpers.js'
import {NotAType, Returns, TypeOfTypes} from '#core/Type.js'
import {Any} from "#core/TypePatterns.js"
import {boolnum} from "#number/helpers.js"
export const zero = onType(
Any, (math, T) => {
export const zero = [
match(Any, (math, T) => {
const z = math.zero(T)
return Returns(T, () => z)
},
TypeOfTypes, Returns(NotAType, t => {
}),
match(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) => {
export const one = [
match(Any, (math, T) => {
const unit = math.one(T)
return Returns(T, () => unit)
},
TypeOfTypes, Returns(NotAType, t => {
}),
match(TypeOfTypes, Returns(NotAType, t => {
if ('one' in t) return t.one
throw new RangeError(
`type '${t}' has no unit element designated as "one"`)
})
)
}))
]
export const hasnan = onType(
Any, (math, T) => {
export const hasnan = [
match(Any, (math, T) => {
const answer = math.hasnan(T)
return Returns(math.typeOf(answer), () => answer)
},
TypeOfTypes, boolnum(t => 'nan' in t)
)
}),
match(TypeOfTypes, boolnum(t => 'nan' in t))
]
export const nan = onType(
Any, (math, T) => {
export const nan = [
match(Any, (math, T) => {
const notanum = math.nan(T)
return Returns(T, () => notanum)
},
TypeOfTypes, Returns(NotAType, t => {
}),
match(TypeOfTypes, Returns(NotAType, t => {
if ('nan' in t) return t.nan
throw new RangeError(
`type '${t}' has no "not a number" element`)
})
)
}))
]

View file

@ -1,8 +1,8 @@
import {onType} from '#core/helpers.js'
import {match} from '#core/helpers.js'
import {Returns} from '#core/Type.js'
import {Any} from '#core/TypePatterns.js'
export const square = onType(Any, (math, T) => {
export const square = match(Any, (math, T) => {
const mult = math.multiply.resolve([T, T])
return Returns(mult.returns, a => mult(a, a))
})

View file

@ -1,5 +1,5 @@
import {ImplementationsGenerator, onType} from '#core/helpers.js'
import {ImplementationsGenerator, match} from '#core/helpers.js'
import {Passthru} from '#core/TypePatterns.js'
export const config = new ImplementationsGenerator(
() => onType(Passthru, {relTol: 1e-12, absTol: 1e-15}))
() => match(Passthru, {relTol: 1e-12, absTol: 1e-15}))

View file

@ -1,10 +1,10 @@
import {ReturnsAs} from './helpers.js'
import {onType} from '#core/helpers.js'
import {match} from '#core/helpers.js'
import {Returns} from '#core/Type.js'
import {Any, matched} from '#core/TypePatterns.js'
import {boolnum} from '#number/helpers.js'
export const equal = onType([Any, Any], (math, [T, U]) => {
export const equal = match([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
@ -38,7 +38,7 @@ export const equal = onType([Any, Any], (math, [T, U]) => {
// now that we have `equal` and `exceeds`, pretty much everything else should
// be easy:
export const compare = onType([Any, Any], (math, [T, U]) => {
export const compare = match([Any, Any], (math, [T, U]) => {
const eq = math.equal.resolve([T, U])
const gt = math.exceeds.resolve([T, U])
const zero = math.zero(T) // asymmetry here is unfortunate, but we have
@ -70,7 +70,7 @@ export const compare = onType([Any, Any], (math, [T, U]) => {
})
})
export const sign = onType(Any, (math, T) => {
export const sign = match(Any, (math, T) => {
const zero = math.zero(T)
const comp = math.compare.resolve([T, T])
return ReturnsAs(comp, t => comp(t, zero))
@ -81,25 +81,25 @@ export const unequal = (math, types) => {
return ReturnsAs(eq, (...args) => !eq(...args))
}
export const larger = onType([Any, Any], (math, [T, U]) => {
export const larger = match([Any, Any], (math, [T, U]) => {
const eq = math.equal.resolve([T, U])
const bigger = math.exceeds.resolve([T, U])
return boolnum((t, u) => !eq(t, u) && bigger(t, u))(math)
})
export const largerEq = onType([Any, Any], (math, [T, U]) => {
export const largerEq = match([Any, Any], (math, [T, U]) => {
const eq = math.equal.resolve([T, U])
const bigger = math.exceeds.resolve([T, U])
return ReturnsAs(bigger, (t, u) => eq(t, u) || bigger(t, u))
})
export const smaller = onType([Any, Any], (math, [T, U]) => {
export const smaller = match([Any, Any], (math, [T, U]) => {
const eq = math.equal.resolve([T, U])
const bigger = math.exceeds.resolve([U, T])
return boolnum((t, u) => !eq(t, u) && bigger(u, t))(math)
})
export const smallerEq = onType([Any, Any], (math, [T, U]) => {
export const smallerEq = match([Any, Any], (math, [T, U]) => {
const eq = math.equal.resolve([T, U])
const bigger = math.exceeds.resolve([U, T])
return ReturnsAs(bigger, (t, u) => eq(t, u) || bigger(u, t))

View file

@ -1,5 +1,5 @@
import {ReturnsAs} from './helpers.js'
import {onType, ResolutionError} from '#core/helpers.js'
import {ResolutionError} from '#core/helpers.js'
import {Returns} from '#core/Type.js'
import {Any} from "#core/TypePatterns.js"

View file

@ -1,9 +1,9 @@
import {Type} from '#core/Type.js'
import {onType} from '#core/helpers.js'
import {match} from '#core/helpers.js'
import {BooleanT} from '#boolean/BooleanT.js'
export const NumberT = new Type(n => typeof n === 'number', {
from: onType(BooleanT, math => math.number.resolve([BooleanT])),
from: match(BooleanT, math => math.number.resolve([BooleanT])),
one: 1,
zero: 0,
nan: NaN

View file

@ -14,9 +14,9 @@ describe('NumberT Type', () => {
it('can convert from BooleanT to NumberT', () => {
const convertImps = NumberT.from
let cnvBtoN
for (const [pattern, convFactory] of convertImps.patterns) {
for (const {pattern, does} of convertImps) {
if (pattern.match([BooleanT])[0] === 1) {
cnvBtoN = convFactory(math)
cnvBtoN = does(math)
break
}
}

View file

@ -1,9 +1,9 @@
import {NumberT} from './NumberT.js'
import {onType} from '#core/helpers.js'
import {match} from '#core/helpers.js'
import {Returns} from '#core/Type.js'
export const plain = f => onType(
export const plain = f => match(
Array(f.length).fill(NumberT), Returns(NumberT, f))
// Takes a behavior returning boolean, and returns a factory

View file

@ -1,4 +1,4 @@
import {onType} from '#core/helpers.js'
import {match} from '#core/helpers.js'
import {Returns} from '#core/Type.js'
import {Optional} from '#core/TypePatterns.js'
import {boolnum} from './helpers.js'
@ -11,7 +11,7 @@ import {NumberT} from './NumberT.js'
// Notice a feature of TypedDispatcher: if you specify one tolerance, you must
// specify both.
export const indistinguishable = onType(
export const indistinguishable = match(
[NumberT, NumberT, Optional([NumberT, NumberT])],
boolnum((a, b, [tolerances = [0, 0]]) => {
const [relTol, absTol] = tolerances
@ -34,4 +34,4 @@ export const indistinguishable = onType(
// Returns truthy if a (interpreted as completely precise) represents a
// greater value than b (interpreted as completely precise). Note that even if
// so, a and b might be indistinguishable() to some tolerances.
export const exceeds = onType([NumberT, NumberT], boolnum((a, b) => a > b))
export const exceeds = match([NumberT, NumberT], boolnum((a, b) => a > b))

View file

@ -1,13 +1,14 @@
import {plain} from './helpers.js'
import {BooleanT} from '#boolean/BooleanT.js'
import {match} from '#core/helpers.js'
import {Returns} from '#core/Type.js'
import {NumberT} from '#number/NumberT.js'
const num = f => Returns(NumberT, f)
export const number = plain(a => a)
number.also(
export const number = [
plain(a => a),
// conversions from Boolean should be consistent with one and zero:
BooleanT, num(p => p ? NumberT.one : NumberT.zero),
[], num(() => 0)
)
match(BooleanT, num(p => p ? NumberT.one : NumberT.zero)),
match([], num(() => 0))
]

View file

@ -2,7 +2,7 @@ import {plain, boolnum} from './helpers.js'
import {NumberT} from './NumberT.js'
import {Returns} from '#core/Type.js'
import {onType} from '#core/helpers.js'
import {match} from '#core/helpers.js'
export const clone = plain(a => a)
export const isnan = onType(NumberT, boolnum(isNaN))
export const isnan = match(NumberT, boolnum(isNaN))