From edfba089e3b07c7b1fee1949a77b7dcb8b012cfa Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Fri, 2 May 2025 19:03:54 -0700 Subject: [PATCH] feat: Complete relational functions for vectors Toward its goal, this commit also: * Adds a new section of logical functions, and defines `not`, `and`, `or` for all current types. * Extends `OneOf` choice/union type to allow argument types that are themselves `OneOf` types. * Adds a readable .toString() method for TypePatterns. * Defines negate (as a no-op) and isnan (as always true) for the Undefined type * Extends comparisons to the Undefined type (to handle comparing vectors of different lengths) --- src/boolean/all.js | 1 + src/boolean/logical.js | 9 ++++ src/core/Type.js | 17 ++++-- src/core/TypeDispatcher.js | 9 +++- src/core/TypePatterns.js | 8 +++ src/coretypes/all.js | 1 + src/coretypes/arithmetic.js | 6 +++ src/generic/__test__/relational.spec.js | 2 +- src/generic/all.js | 1 + src/generic/logical.js | 40 ++++++++++++++ src/generic/relational.js | 19 ++++--- src/number/all.js | 1 + src/number/logical.js | 6 +++ src/number/relational.js | 11 +++- src/vector/__test__/relational.spec.js | 13 +++++ src/vector/all.js | 1 + src/vector/helpers.js | 70 +++++++++++++++++++++++-- src/vector/logical.js | 7 +++ src/vector/relational.js | 35 +++++++++++-- 19 files changed, 238 insertions(+), 19 deletions(-) create mode 100644 src/boolean/logical.js create mode 100644 src/coretypes/arithmetic.js create mode 100644 src/generic/logical.js create mode 100644 src/number/logical.js create mode 100644 src/vector/logical.js diff --git a/src/boolean/all.js b/src/boolean/all.js index be70fad..1711e64 100644 --- a/src/boolean/all.js +++ b/src/boolean/all.js @@ -1,3 +1,4 @@ export * as typeDefinition from './BooleanT.js' +export * as logical from './logical.js' export * as type from './type.js' export * as utilities from './utils.js' diff --git a/src/boolean/logical.js b/src/boolean/logical.js new file mode 100644 index 0000000..2148018 --- /dev/null +++ b/src/boolean/logical.js @@ -0,0 +1,9 @@ +import {BooleanT} from './BooleanT.js' +import {Returns} from '#core/Type.js' +import {match} from '#core/TypePatterns.js' + +export const not = match(BooleanT, Returns(BooleanT, p => !p)) +export const and = match( + [BooleanT, BooleanT], Returns(BooleanT, (p, q) => p && q)) +export const or = match( + [BooleanT, BooleanT], Returns(BooleanT, (p, q) => p || q)) diff --git a/src/core/Type.js b/src/core/Type.js index 398aaaa..daa8ca7 100644 --- a/src/core/Type.js +++ b/src/core/Type.js @@ -71,21 +71,32 @@ NotAType._doNotMerge = true const unionDirectory = new ArrayKeyedMap() // make sure only one of each union export const OneOf = (...types) => { + if (!types.length) { + throw new RangeError('cannot choose OneOf no types at all') + } const nonType = types.findIndex(T => !(T instanceof Type)) if (nonType >= 0) { throw new RangeError( `OneOf can only take type arguments, not ${types[nonType]}`) } - const typeSet = new Set(types) // remove duplicates + const typeSet = new Set() // remove duplicates: + for (const type of types) { + if (type.unifies) { + type.unifies.forEach(t => typeSet.add(t)) + } else typeSet.add(type) + } + if (typeSet.size === 1) return types[0] const typeList = Array.from(typeSet).sort() // canonical order const generic = typeList.find(T => !T.concrete) if (generic) { throw new RangeError(`OneOf can only take concrete types, not ${generic}`) } if (!unionDirectory.has(typeList)) { - unionDirectory.set(typeList, new Type( + const unionType = new Type( t => typeList.some(T => T.test(t)), - {typeName: typeList.join('|')})) + {typeName: typeList.join('|')}) + unionType.unifies = typeList + unionDirectory.set(typeList, unionType) } return unionDirectory.get(typeList) } diff --git a/src/core/TypeDispatcher.js b/src/core/TypeDispatcher.js index c56e39d..941d888 100644 --- a/src/core/TypeDispatcher.js +++ b/src/core/TypeDispatcher.js @@ -309,9 +309,9 @@ export class TypeDispatcher { let needItem = true let item let template + let pattern if (imps.length) { for (const options of [{}, {convert: true}]) { - let pattern for ([pattern, item] of imps) { let finalIndex ;[finalIndex, template] = pattern.match(types, options) @@ -327,11 +327,18 @@ export class TypeDispatcher { needItem = false item = this._fallbacks[key] template = types + pattern = Passthru } if (needItem) { throw new ResolutionError( `no matching definition of '${key}' on '${types}'`) } + /* The following message is often helpful in debugging, so left it + in but commented out; likely at some point we should have some + sort of trace facility that this would be a part of: + */ + // console.log(`RESOLVING ${key} on ${types} matches ${pattern}`) + // If this key is producing a non-function value, we're done if (!isPlainFunction(item)) { behave.set(bhvix, item) diff --git a/src/core/TypePatterns.js b/src/core/TypePatterns.js index b3cb566..a67a3b9 100644 --- a/src/core/TypePatterns.js +++ b/src/core/TypePatterns.js @@ -9,6 +9,7 @@ export class TypePattern { throw new Error('Specific TypePatterns must implement sampleTypes') } equal(other) {return other.constructor === this.constructor} + toString() {return 'Abstract Pattern (?!)'} } class MatchTypePattern extends TypePattern { @@ -34,6 +35,7 @@ class MatchTypePattern extends TypePattern { } sampleTypes() {return [this.type]} equal(other) {return super.equal(other) && this.type === other.type} + toString() {return `Match(${this.type})`} } class SequencePattern extends TypePattern { @@ -64,6 +66,7 @@ class SequencePattern extends TypePattern { && this.patterns.length === other.patterns.length && this.patterns.every((elt, ix) => elt.equal(other.patterns[ix])) } + toString() {return `[${this.patterns}]`} } class PredicatePattern extends TypePattern { @@ -84,6 +87,7 @@ class PredicatePattern extends TypePattern { equal(other) { return super.equal(other) && this.predicate === other.predicate } + toString() {return `Test(${this.predicate})`} } export const pattern = patternOrSpec => { @@ -106,6 +110,7 @@ class AnyPattern extends TypePattern { : [-1, Undefined] } sampleTypes() {return [Undefined]} + toString() {return 'Any'} } export const Any = new AnyPattern() @@ -132,6 +137,7 @@ class OptionalPattern extends TypePattern { equal(other) { return super.equal(other) && this.pattern.equal(other.pattern) } + toString() {return `?${this.pattern}`} } export const Optional = item => new OptionalPattern(item) @@ -158,6 +164,7 @@ class MultiPattern extends TypePattern { equal(other) { return super.equal(other) && this.pattern.equal(other.pattern) } + toString() {return `${this.pattern}*`} } export const Multiple = item => new MultiPattern(item) @@ -170,6 +177,7 @@ class PassthruPattern extends TypePattern { return [typeSequence.length, typeSequence.slice(position)] } sampleTypes() {return []} + toString() {return 'Passthru'} } export const Passthru = new PassthruPattern() diff --git a/src/coretypes/all.js b/src/coretypes/all.js index 18b23af..0f81e87 100644 --- a/src/coretypes/all.js +++ b/src/coretypes/all.js @@ -1,2 +1,3 @@ +export * from './arithmetic.js' export * from './relational.js' export * from './utils.js' diff --git a/src/coretypes/arithmetic.js b/src/coretypes/arithmetic.js new file mode 100644 index 0000000..1e2f279 --- /dev/null +++ b/src/coretypes/arithmetic.js @@ -0,0 +1,6 @@ +import {Returns, Undefined} from '#core/Type.js' +import {match} from '#core/TypePatterns.js' +import {boolnum} from '#number/helpers.js' + +export const isnan = match(Undefined, boolnum(() => true)) +export const negate = match(Undefined, Returns(Undefined, () => undefined)) diff --git a/src/generic/__test__/relational.spec.js b/src/generic/__test__/relational.spec.js index 18c2f95..b3ac1aa 100644 --- a/src/generic/__test__/relational.spec.js +++ b/src/generic/__test__/relational.spec.js @@ -48,7 +48,7 @@ describe('generic relational functions', () => { assert.throws(() => compare(false, NaN), TypeError) assert(isNaN(compare(NaN, NaN))) assert.throws(() => compare(true, false), TypeError) - assert.throws(() => compare(undefined, -1), ResolutionError) + assert.throws(() => compare(math.types.BooleanT, -1), ResolutionError) }) it('determines the sign of numeric values', () => { const {sign} = math diff --git a/src/generic/all.js b/src/generic/all.js index 89d8802..5560d5d 100644 --- a/src/generic/all.js +++ b/src/generic/all.js @@ -1,3 +1,4 @@ export * as arithmetic from './arithmetic.js' +export * as logical from './logical.js' export * as relational from './relational.js' export * as utilities from './utils.js' diff --git a/src/generic/logical.js b/src/generic/logical.js new file mode 100644 index 0000000..df90914 --- /dev/null +++ b/src/generic/logical.js @@ -0,0 +1,40 @@ +import {ReturnsAs} from './helpers.js' + +import {OneOf, Returns} from '#core/Type.js' +import {Any, Multiple, match} from '#core/TypePatterns.js' +import {boolnum} from '#number/helpers.js' + +export const not = match(Any, (math, T) => { + const bool = math.boolean.resolve(T) + return ReturnsAs(bool, t => !bool(t)) +}) + +export const and = [ + match([], boolnum(() => true)), + match(Any, (_math, T) => Returns(T, t => t)), + match([Any, Any], (math, [T, U]) => { + const bool = math.boolean.resolve(T) + return Returns(OneOf(T, U), (t, u) => bool(t) ? u : t) + }), + match([Any, Any, Any, Multiple(Any)], (math, [T, U, V, Rest]) => { + const andRest = math.and.resolve([U, V, ...Rest]) + const andFirst = math.and.resolve([T, andRest.returns]) + return ReturnsAs( + andFirst, (t, u, v, rest) => andFirst(t, andRest(u, v, ...rest))) + }) +] + +export const or = [ + match([], boolnum(() => false)), + match(Any, (_math, T) => Returns(T, t => t)), + match([Any, Any], (math, [T, U]) => { + const bool = math.boolean.resolve(T) + return Returns(OneOf(T, U), (t, u) => bool(t) ? t : u) + }), + match([Any, Any, Any, Multiple(Any)], (math, [T, U, V, Rest]) => { + const orRest = math.or.resolve([U, V, ...Rest]) + const orFirst = math.and.resolve([T, orRest.returns]) + return ReturnsAs( + orFirst, (t, u, v, rest) => orFirst(t, orRest(u, v, ...rest))) + }) +] diff --git a/src/generic/relational.js b/src/generic/relational.js index 0455ddb..15ce19a 100644 --- a/src/generic/relational.js +++ b/src/generic/relational.js @@ -82,35 +82,42 @@ export const sign = match(Any, (math, T) => { export const unequal = match(Passthru, (math, types) => { const eq = math.equal.resolve(types) - return ReturnsAs(eq, (...args) => !eq(...args)) + const not = math.not.resolve(eq.returns) + return ReturnsAs(not, (...args) => not(eq(...args))) }) 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) + const not = math.not.resolve(eq.returns) + const and = math.and.resolve([not.returns, bigger.returns]) + return ReturnsAs(and, (t, u) => and(not(eq(t, u)), bigger(t, u))) }) export const isPositive = match(Any, (math, T) => { const zero = math.zero(T) const larger = math.larger.resolve([T, T]) - return boolnum(t => larger(t, zero)) + return ReturnsAs(larger, t => larger(t, zero)) }) 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)) + const or = math.or.resolve([eq.returns, bigger.returns]) + return ReturnsAs(or, (t, u) => or(eq(t, u), bigger(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) + const not = math.not.resolve(eq.returns) + const and = math.and.resolve([not.returns, bigger.returns]) + return ReturnsAs(and, (t, u) => and(not(eq(t, u)), bigger(u, t))) }) 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)) + const or = math.or.resolve([eq.returns, bigger.returns]) + return ReturnsAs(or, (t, u) => or(eq(t, u), bigger(u, t))) }) diff --git a/src/number/all.js b/src/number/all.js index 7872d78..df37bac 100644 --- a/src/number/all.js +++ b/src/number/all.js @@ -1,5 +1,6 @@ export * as typeDefinition from './NumberT.js' export * as arithmetic from './arithmetic.js' +export * as logical from './logical.js' export * as relational from './relational.js' export * as type from './type.js' export * as utils from './utils.js' diff --git a/src/number/logical.js b/src/number/logical.js new file mode 100644 index 0000000..6d6311b --- /dev/null +++ b/src/number/logical.js @@ -0,0 +1,6 @@ +import {boolnum} from './helpers.js' +import {NumberT} from './NumberT.js' + +import {match} from '#core/TypePatterns.js' + +export const not = match(NumberT, boolnum(a => !a)) diff --git a/src/number/relational.js b/src/number/relational.js index 2d2e8df..c9d621d 100644 --- a/src/number/relational.js +++ b/src/number/relational.js @@ -1,7 +1,9 @@ -import {match, Optional} from '#core/TypePatterns.js' import {boolnum} from './helpers.js' import {NumberT} from './NumberT.js' +import {Undefined} from '#core/Type.js' +import {match, Optional} from '#core/TypePatterns.js' + // In nanomath, we take the point of view that two comparators are primitive: // indistinguishable(a, b, relTol, absTol), and exceeds(a, b). All others // are defined generically in terms of these. They typically return BooleanT, @@ -32,4 +34,9 @@ export const indistinguishable = match( // 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 = match([NumberT, NumberT], boolnum((a, b) => a > b)) +export const exceeds = [ + match([NumberT, NumberT], boolnum((a, b) => a > b)), + // Needed to allow comparison of vectors of different lengths: + match([Undefined, NumberT], boolnum(() => false)), + match([NumberT, Undefined], boolnum(() => false)) +] diff --git a/src/vector/__test__/relational.spec.js b/src/vector/__test__/relational.spec.js index 3637b86..a6e8871 100644 --- a/src/vector/__test__/relational.spec.js +++ b/src/vector/__test__/relational.spec.js @@ -31,4 +31,17 @@ describe('Vector relational functions', () => { assert.deepStrictEqual( eq([[1, 2], [3, 4]], [[1, 3], [2, 4]]), [[t, f], [f, t]]) }) + it('performs order comparisons elementwise', () => { + const pyth = [3, 4, 5] + const ans = [-1, 0, 1] + const powers = [2, 4, 8] + assert.deepStrictEqual(math.compare(pyth, [5, 4, 3]), ans) + assert.deepStrictEqual(math.sign([-Infinity, 0, 3]), ans) + assert.deepStrictEqual(math.unequal(pyth, [5, 4 + 1e-14, 5]), [t, f, f]) + assert.deepStrictEqual(math.larger(pyth, powers), [t, f, f]) + assert.deepStrictEqual(math.isPositive(ans), [f, f, t]) + assert.deepStrictEqual(math.largerEq(pyth, powers), [t, t, f]) + assert.deepStrictEqual(math.smaller(pyth, powers), [f, f, t]) + assert.deepStrictEqual(math.smallerEq(pyth, powers), [f, t, t]) + }) }) diff --git a/src/vector/all.js b/src/vector/all.js index 374601f..bd58b0b 100644 --- a/src/vector/all.js +++ b/src/vector/all.js @@ -1,4 +1,5 @@ export * as typeDefinition from './Vector.js' +export * as logical from './logical.js' export * as relational from './relational.js' export * as type from './type.js' export * as utilities from './utils.js' diff --git a/src/vector/helpers.js b/src/vector/helpers.js index 2b52280..06ada71 100644 --- a/src/vector/helpers.js +++ b/src/vector/helpers.js @@ -1,6 +1,6 @@ import {Vector} from './Vector.js' -import {NotAType, Returns} from '#core/Type.js' -import {match} from '#core/TypePatterns.js' +import {NotAType, Returns, Undefined} from '#core/Type.js' +import {Any, match} from '#core/TypePatterns.js' export const promoteUnary = name => match(Vector, (math, V, strategy) => { if (V.Component === NotAType) { @@ -12,4 +12,68 @@ export const promoteUnary = name => match(Vector, (math, V, strategy) => { return Returns(Vector(compOp.returns), v => v.map(elt => compOp(elt))) }) - +export const promoteBinary = name => [ + match([Vector, Any], (math, [V, E], strategy) => { + const VComp = V.Component + if (VComp === NotAType) { + return Returns(V, (v, e) => v.map( + f => math.resolve(name, [math.typeOf(f), E], strategy)(f, e))) + } + const compOp = math.resolve(name, [VComp, E], strategy) + return Returns( + Vector(compOp.returns), (v, e) => v.map(f => compOp(f, e))) + }), + match([Any, Vector], (math, [E, V], strategy) => { + const VComp = V.Component + if (VComp === NotAType) { + return Returns(V, (e, v) => v.map( + f => math.resolve(name, [E, math.typeOf(f)], strategy)(e, f))) + } + const compOp = math.resolve(name, [E, VComp], strategy) + return Returns( + Vector(compOp.returns, (e, v) => v.map(f => compOp(e, f)))) + }), + match([Vector, Vector], (math, [V, W], strategy) => { + const VComp = V.Component + const WComp = W.Component + let compOp + let opNoV + let opNoW + let ReturnType + if (VComp === NotAType || WComp === NotAType) { + const typ = math.typeOf + compOp = (v, w) => { + return math.resolve(name, [typ(v), typ(w)], strategy)(v, w) + } + opNoV = compOp + opNoW = compOp + ReturnType = Vector(NotAType) + } else { + compOp = math.resolve(name, [VComp, WComp], strategy) + opNoV = math.resolve(name, [Undefined, WComp], strategy) + opNoW = math.resolve(name, [VComp, Undefined], strategy) + ReturnType = Vector(compOp.returns) + } + return Returns( + ReturnType, + (v, w) => { + const vInc = Number(v.length > 1) + const wInc = Number(w.length >= v.length || w.length > 1) + const retval = [] + let vIx = 0 + let wIx = 0 + while ((vInc && vIx < v.length) + || (wInc && wIx < w.length) + ) { + if (vIx >= v.length) { + retval.push(opNoV(undefined, w[wIx])) + } else if (wIx >= w.length) { + retval.push(opNoW(v[vIx], undefined)) + } else retval.push(compOp(v[vIx], w[wIx])) + vIx += vInc + wIx += wInc + } + return retval + }) + }) +] diff --git a/src/vector/logical.js b/src/vector/logical.js new file mode 100644 index 0000000..26db8cc --- /dev/null +++ b/src/vector/logical.js @@ -0,0 +1,7 @@ +import {promoteBinary, promoteUnary} from './helpers.js' + +export const not = promoteUnary('not') +export const and = promoteBinary('and') +export const or = promoteBinary('or') + + diff --git a/src/vector/relational.js b/src/vector/relational.js index 23e5835..97e41a0 100644 --- a/src/vector/relational.js +++ b/src/vector/relational.js @@ -1,4 +1,6 @@ import {Vector} from './Vector.js' +import {promoteBinary} from './helpers.js' + import {NotAType, Returns, Undefined} from '#core/Type.js' import {Any, Optional, match} from '#core/TypePatterns.js' import {BooleanT} from '#boolean/BooleanT.js' @@ -23,11 +25,21 @@ export const indistinguishable = [ match([Vector, Any, Optional([Any, Any])], (math, [V, E, T]) => { const VComp = V.Component if (T.length === 0) { // no tolerances + if (VComp === NotAType) { + return Returns(V, (v, e) => v.map(f => { + return math.indistinguishable.resolve([math.typeOf(f), E])(f, e) + })) + } const same = math.indistinguishable.resolve([VComp, E]) return Returns( Vector(same.returns), (v, e) => v.map(f => same(f, e))) } const [[RT, AT]] = T + if (VComp === NotAType) { + return Returns(V, (v, e, [[rT, aT]]) => v.map(f => { + return math.indistinguishable(f, e, rT, aT) + })) + } const same = math.indistinguishable.resolve([VComp, E, RT, AT]) return Returns( Vector(same.returns), @@ -38,11 +50,21 @@ export const indistinguishable = [ // same is symmetric, even though it probably is const VComp = V.Component if (T.length === 0) { // no tolerances + if (VComp === NotAType) { + return Returns(V, (e, v) => v.map(f => { + return math.indistinguishable.resolve([E, math.typeOf(f)])(e, f) + })) + } const same = math.indistinguishable.resolve([E, VComp]) return Returns( Vector(same.returns), (e, v) => v.map(f => same(e, f))) } const [[RT, AT]] = T + if (VComp === NotAType) { + return Returns(V, (e, v, [[rT, aT]]) => v.map(f => { + return math.indistiguishable(e, f, rT, aT) + })) + } const same = math.indistinguishable.resolve([E, VComp, RT, AT]) return Returns( Vector(same.returns), @@ -51,10 +73,15 @@ export const indistinguishable = [ match([Vector, Vector, Optional([Any, Any])], (math, [V, W, T]) => { const VComp = V.Component const WComp = W.Component + const inhomogeneous = VComp === NotAType || WComp === NotAType let same let sameNoV let sameNoW - if (T.length === 0) { // no tolerances + if (inhomogeneous) { + same = math.indistinguishable + sameNoV = same + sameNoW = same + } else if (T.length === 0) { // no tolerances same = math.indistinguishable.resolve([VComp, WComp]) sameNoV = math.indistinguishable.resolve([Undefined, WComp]) sameNoW = math.indistinguishable.resolve([VComp, Undefined]) @@ -65,7 +92,7 @@ export const indistinguishable = [ sameNoW = math.indistinguishable.resolve([VComp, Undefined, RT, AT]) } return Returns( - Vector(same.returns), + inhomogeneous ? Vector(NotAType) : Vector(same.returns), (v, w, [tol = [0, 0]]) => { const [rT, aT] = tol const vInc = Number(v.length > 1) @@ -73,7 +100,6 @@ export const indistinguishable = [ const retval = [] let vIx = 0 let wIx = 0 - let remainder = vIx < v.length || wIx < w.length while ((vInc && vIx < v.length) || (wInc && wIx < w.length) ) { @@ -89,3 +115,6 @@ export const indistinguishable = [ }) }) ] + +export const compare = promoteBinary('compare') +export const exceeds = promoteBinary('exceeds')