refactor: prepare for boolean functions
All checks were successful
/ test (pull_request) Successful in 19s

* Defines a BooleanT type
  * adds options to the Type constructor, so far just to allow conversions
    from other types
  * renames Number type to NumberT
  * records the name of types, and puts the name in the string representation
  * defines a conversion fron BooleanT to NumberT, specified to be automatic
  * stub code for automatic conversions, not yet complete
  * BooleanT not yet added to nanomath

Checked that the new facilities do not disrupt the prior behavior on numbers.
This commit is contained in:
Glen Whitney 2025-04-10 13:57:05 -07:00
parent 14011984a0
commit 5bee93dbb3
16 changed files with 123 additions and 62 deletions

3
src/boolean/BooleanT.js Normal file
View file

@ -0,0 +1,3 @@
import {Type} from '#core/Type.js'
export const BooleanT = new Type(n => typeof n === 'boolean')

View file

@ -3,8 +3,13 @@ const typeObject = {} // have to make sure there is only one
export const types = () => typeObject export const types = () => typeObject
export class Type { export class Type {
constructor(f) { constructor(f, options = {}) {
this.test = f this.test = f
this.from = options.from ?? []
this.from.push(this)
}
toString() {
return this.name || `[Type ${this.test}]`
} }
} }

View file

@ -3,9 +3,12 @@ import {typeOf, Type, bootstrapTypes} from './Type.js'
import { import {
Implementations, isPlainFunction, isPlainObject, onType Implementations, isPlainFunction, isPlainObject, onType
} from './helpers.js' } from './helpers.js'
import {alwaysMatches, Any, Multiple} from './TypePatterns.js' import {
alwaysMatches, matched, needsCollection, Any, Multiple
} from './TypePatterns.js'
// helper that organizes a list into the same chunks as a pattern is // helper that organizes a list into the same chunks as a pattern is
// NOTE NOTE: need to update to handle conversions!
const collectLike = (list, pattern, state = {pos: 0}) => { const collectLike = (list, pattern, state = {pos: 0}) => {
const result = [] const result = []
for (const elt of pattern) { for (const elt of pattern) {
@ -41,6 +44,7 @@ export class TypeDispatcher {
if (val instanceof Type) { if (val instanceof Type) {
// TODO: Need to wipe out any dependencies on types[key]! // TODO: Need to wipe out any dependencies on types[key]!
this.types[key] = val this.types[key] = val
val.name = key
continue continue
} }
if (typeof val === 'function') { if (typeof val === 'function') {
@ -161,13 +165,16 @@ export class TypeDispatcher {
let pattern let pattern
let template let template
if (imps.length) { if (imps.length) {
for ([pattern, item] of imps) { for (const options of [{}, {convert: true}]) {
let finalIndex for ([pattern, item] of imps) {
;[finalIndex, template] = pattern.match(types) let finalIndex
if (finalIndex === types.length) { ;[finalIndex, template] = pattern.match(types, options)
needItem = false if (finalIndex === types.length) {
break needItem = false
break
}
} }
if (!needItem) break
} }
} }
if (needItem && key in this._fallbacks) { if (needItem && key in this._fallbacks) {
@ -190,7 +197,8 @@ export class TypeDispatcher {
this.resolve._genDepsOf.push([key, types]) this.resolve._genDepsOf.push([key, types])
let theBehavior = () => undefined let theBehavior = () => undefined
try { try {
theBehavior = item(DependencyRecorder(this, [], this), template) theBehavior = item(
DependencyRecorder(this, [], this), matched(template))
} catch { } catch {
behave.set(types, item) behave.set(types, item)
return item return item
@ -199,8 +207,7 @@ export class TypeDispatcher {
} }
if (typeof theBehavior === 'function' if (typeof theBehavior === 'function'
&& theBehavior.length && theBehavior.length
&& Array.isArray(template) && needsCollection(template)
&& template.some(elt => Array.isArray(elt))
) { ) {
// have to wrap the behavior to collect the actual arguments // have to wrap the behavior to collect the actual arguments
// in the way corresponding to the template // in the way corresponding to the template

View file

@ -1,7 +1,7 @@
import {Type, Undefined} from './Type.js' import {Type, Undefined} from './Type.js'
export class TypePattern { export class TypePattern {
match(typeSequence, position = 0) { match(typeSequence, options={}) {
throw new Error('Specific TypePatterns must implement match') throw new Error('Specific TypePatterns must implement match')
} }
sampleTypes() { sampleTypes() {
@ -15,10 +15,16 @@ class MatchTypePattern extends TypePattern {
super() super()
this.type = typeToMatch this.type = typeToMatch
} }
match(typeSequence, position = 0) { match(typeSequence, options={}) {
const position = options.position ?? 0
const allowed = options.convert ? this.type.from : [this.type]
if (position < typeSequence.length if (position < typeSequence.length
&& typeSequence[position] === this.type && allowed.includes(typeSequence[position])
) { ) {
let templateItem = typeSequence[position]
if (templateItem !== this.type) {
templateItem = {actual: templateItem, matched: this.type}
}
return [position + 1, typeSequence[position]] return [position + 1, typeSequence[position]]
} }
return [-1, Undefined] return [-1, Undefined]
@ -32,15 +38,19 @@ class SequencePattern extends TypePattern {
super() super()
this.patterns = itemsToMatch.map(pattern) this.patterns = itemsToMatch.map(pattern)
} }
match(typeSequence, position = 0) { match(typeSequence, options={_internal: true}) {
options = options._internal
? options
: Object.assign({_internal: true}, options)
options.position ??= 0
const matches = [] const matches = []
for (const pat of this.patterns) { for (const pat of this.patterns) {
const [newPos, newMatch] = pat.match(typeSequence, position) const [newPos, newMatch] = pat.match(typeSequence, options)
if (newPos < 0) return [-1, Undefined] if (newPos < 0) return [-1, Undefined]
position = newPos options.position = newPos
matches.push(newMatch) matches.push(newMatch)
} }
return [position, matches] return [options.position, matches]
} }
sampleTypes() { sampleTypes() {
return this.patterns.map(pat => pat.sampleTypes()).flat() return this.patterns.map(pat => pat.sampleTypes()).flat()
@ -62,7 +72,8 @@ export const pattern = patternOrSpec => {
} }
class AnyPattern extends TypePattern { class AnyPattern extends TypePattern {
match(typeSequence, position = 0) { match(typeSequence, options={}) {
const position = options.position ?? 0
return position < typeSequence.length return position < typeSequence.length
? [position + 1, typeSequence[position]] ? [position + 1, typeSequence[position]]
: [-1, Undefined] : [-1, Undefined]
@ -77,14 +88,18 @@ class OptionalPattern extends TypePattern {
super() super()
this.pattern = pattern(item) this.pattern = pattern(item)
} }
match(typeSequence, position = 0) { match(typeSequence, options={_internal: true}) {
options = options._internal
? options
: Object.assign({_internal: true}, options)
options.position ??= 0
const matches = [] const matches = []
const [newPos, newMatch] = this.pattern.match(typeSequence, position) const [newPos, newMatch] = this.pattern.match(typeSequence, options)
if (newPos >= 0) { if (newPos >= 0) {
position = newPos options.position = newPos
matches.push(newMatch) matches.push(newMatch)
} }
return [position, matches] return [options.position, matches]
} }
sampleTypes() {return []} sampleTypes() {return []}
equal(other) { equal(other) {
@ -99,12 +114,16 @@ class MultiPattern extends TypePattern {
super() super()
this.pattern = pattern(item) this.pattern = pattern(item)
} }
match(typeSequence, position = 0) { match(typeSequence, options={_internal: true}) {
options = options._internal
? options
: Object.assign({_internal: true}, options)
options.position ??= 0
const matches = [] const matches = []
while (true) { while (true) {
const [newPos, newMatch] = this.pattern.match(typeSequence, position) const [newPos, newMatch] = this.pattern.match(typeSequence, options)
if (newPos < 0) return [position, matches] if (newPos < 0) return [options.position, matches]
position = newPos options.position = newPos
matches.push(newMatch) matches.push(newMatch)
} }
} }
@ -117,3 +136,18 @@ class MultiPattern extends TypePattern {
export const Multiple = item => new MultiPattern(item) export const Multiple = item => new MultiPattern(item)
export const alwaysMatches = export const alwaysMatches =
pat => pat instanceof MultiPattern && pat.pattern instanceof AnyPattern pat => pat instanceof MultiPattern && pat.pattern instanceof AnyPattern
// returns the template just of matched types, dropping any actual types
export const matched = (template) => {
if (Array.isArray(template)) return template.map(matched)
return template.matched ?? template
}
// checks if the template is just pass-through or needs collection
export const needsCollection = (template) => {
if (Array.isArray(template)) {
return template.some(
elt => Array.isArray(elt) || needsCollection(elt))
}
return 'actual' in template
}

View file

@ -1,14 +1,14 @@
import assert from 'assert' import assert from 'assert'
import math from '#nanomath' import math from '#nanomath'
import {Number} from '#number/Number.js' import {NumberT} from '#number/NumberT.js'
import {Returns} from '../Type.js' import {Returns} from '../Type.js'
import {isPlainFunction} from '../helpers.js' import {isPlainFunction} from '../helpers.js'
describe('Core types', () => { describe('Core types', () => {
it('creates an object with all of the types', () => { it('creates an object with all of the types', () => {
assert('Number' in math.types) assert('NumberT' in math.types)
assert.strictEqual(math.types.Number, Number) assert.strictEqual(math.types.NumberT, NumberT)
assert('Undefined' in math.types) assert('Undefined' in math.types)
assert(!('Type' in math.types)) assert(!('Type' in math.types))
assert(math.types.Undefined.test(undefined)) assert(math.types.Undefined.test(undefined))
@ -18,9 +18,9 @@ describe('Core types', () => {
it('supports a typeOf operator', () => { it('supports a typeOf operator', () => {
const tO = math.typeOf const tO = math.typeOf
assert.strictEqual(tO(7), Number) assert.strictEqual(tO(7), NumberT)
assert.strictEqual(tO(undefined), math.types.Undefined) assert.strictEqual(tO(undefined), math.types.Undefined)
assert.strictEqual(tO(Number), math.types.TypeOfTypes) assert.strictEqual(tO(NumberT), math.types.TypeOfTypes)
assert.throws(() => tO(Symbol()), TypeError) assert.throws(() => tO(Symbol()), TypeError)
}) })

View file

@ -13,8 +13,8 @@ describe('TypeDispatcher', () => {
assert.strictEqual( assert.strictEqual(
incremental.typeOf(undefined), incremental.types.Undefined) incremental.typeOf(undefined), incremental.types.Undefined)
incremental.merge(numbers) incremental.merge(numbers)
const {Number, TypeOfTypes, Undefined} = incremental.types const {NumberT, TypeOfTypes, Undefined} = incremental.types
assert(Number.test(7)) assert(NumberT.test(7))
assert.strictEqual(incremental.add(-1.5, 0.5), -1) assert.strictEqual(incremental.add(-1.5, 0.5), -1)
// Make Undefined act like zero: // Make Undefined act like zero:
incremental.merge({add: onType( incremental.merge({add: onType(
@ -27,18 +27,18 @@ describe('TypeDispatcher', () => {
TypeOfTypes) TypeOfTypes)
assert.strictEqual(incremental.add(undefined, -3.25), -3.25) assert.strictEqual(incremental.add(undefined, -3.25), -3.25)
assert.strictEqual( assert.strictEqual(
incremental.add.resolve([Undefined, Number]).returns, incremental.add.resolve([Undefined, NumberT]).returns,
Number) 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(Number, () => NaN) const alwaysNaN = Returns(NumberT, () => NaN)
incremental.merge({add: onType( incremental.merge({add: onType(
[Undefined, Number], alwaysNaN, [Undefined, NumberT], alwaysNaN,
[Number, Undefined], alwaysNaN [NumberT, Undefined], alwaysNaN
)}) )})
assert(isNaN(incremental.add(undefined, -3.25))) assert(isNaN(incremental.add(undefined, -3.25)))
assert.strictEqual( assert.strictEqual(
incremental.add.resolve([Undefined, Number]).returns, incremental.add.resolve([Undefined, NumberT]).returns,
Number) NumberT)
}) })
it('changes methods when their dependencies change', () => { it('changes methods when their dependencies change', () => {
const gnmath = new TypeDispatcher(generics, numbers) const gnmath = new TypeDispatcher(generics, numbers)

View file

@ -1,13 +1,18 @@
import {Type} from './Type.js'
import {pattern} from './TypePatterns.js' import {pattern} from './TypePatterns.js'
export class Implementations { export class Implementations {
constructor(imps) { constructor(imps) {
this.patterns = new Map() this.patterns = new Map()
this._add(imps)
}
_add(imps) {
for (let i = 0; i < imps.length; ++i) { for (let i = 0; i < imps.length; ++i) {
this.patterns.set(pattern(imps[i]), imps[++i]) this.patterns.set(pattern(imps[i]), imps[++i])
} }
} }
also(...imps) {
this._add(imps)
}
} }
export const onType = (...imps) => new Implementations(imps) export const onType = (...imps) => new Implementations(imps)

View file

@ -5,7 +5,7 @@ describe('generic arithmetic', () => {
it('squares anything', () => { it('squares anything', () => {
assert.strictEqual(math.square(7), 49) assert.strictEqual(math.square(7), 49)
assert.strictEqual( assert.strictEqual(
math.square.resolve([math.types.Number]).returns, math.square.resolve([math.types.NumberT]).returns,
math.types.Number) math.types.NumberT)
}) })
}) })

View file

@ -1,3 +0,0 @@
import {Type} from '#core/Type.js'
export const Number = new Type(n => typeof n === 'number')

6
src/number/NumberT.js Normal file
View file

@ -0,0 +1,6 @@
import {Type} from '#core/Type.js'
import {BooleanT} from '#boolean/BooleanT.js'
export const NumberT = new Type(n => typeof n === 'number', {
from: [BooleanT]
})

View file

@ -1,11 +0,0 @@
import assert from 'assert'
import {Number} from '../Number.js'
describe('Number Type', () => {
it('correctly recognizes numbers', () => {
assert(Number.test(3))
assert(Number.test(NaN))
assert(Number.test(Infinity))
assert(!Number.test("3"))
})
})

View file

@ -0,0 +1,11 @@
import assert from 'assert'
import {NumberT} from '../NumberT.js'
describe('NumberT Type', () => {
it('correctly recognizes numbers', () => {
assert(NumberT.test(3))
assert(NumberT.test(NaN))
assert(NumberT.test(Infinity))
assert(!NumberT.test("3"))
})
})

View file

@ -1,4 +1,4 @@
export * as typeDefinition from './Number.js' export * as typeDefinition from './NumberT.js'
export * as arithmetic from './arithmetic.js' export * as arithmetic from './arithmetic.js'
export * as type from './type.js' export * as type from './type.js'
export * as utils from './utils.js' export * as utils from './utils.js'

View file

@ -1,7 +1,7 @@
import {Number} from './Number.js' import {NumberT} from './NumberT.js'
import {onType} from '#core/helpers.js' import {onType} from '#core/helpers.js'
import {Returns} from '#core/Type.js' import {Returns} from '#core/Type.js'
export const plain = f => onType( export const plain = f => onType(
Array(f.length).fill(Number), Returns(Number, f)) Array(f.length).fill(NumberT), Returns(NumberT, f))

View file

@ -1,4 +1,7 @@
import {plain} from './helpers.js' import {plain} from './helpers.js'
import {BooleanT} from '#boolean/BooleanT.js'
import {Returns} from '#core/Type.js'
import {NumberT} from '#number/NumberT.js'
// Not much to do so far when there is only one type
export const number = plain(a => a) export const number = plain(a => a)
number.also(BooleanT, Returns(NumberT, a => a ? 1 : 0))

View file

@ -1,6 +1,7 @@
{ {
"imports" : { "imports" : {
"#nanomath": "./nanomath.js", "#nanomath": "./nanomath.js",
"#boolean/*.js": "./boolean/*.js",
"#core/*.js": "./core/*.js", "#core/*.js": "./core/*.js",
"#number/*.js": "./number/*.js" "#number/*.js": "./number/*.js"
}, },