refactor: prepare for boolean functions

* 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 class Type {
constructor(f) {
constructor(f, options = {}) {
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 {
Implementations, isPlainFunction, isPlainObject, onType
} 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
// NOTE NOTE: need to update to handle conversions!
const collectLike = (list, pattern, state = {pos: 0}) => {
const result = []
for (const elt of pattern) {
@ -41,6 +44,7 @@ export class TypeDispatcher {
if (val instanceof Type) {
// TODO: Need to wipe out any dependencies on types[key]!
this.types[key] = val
val.name = key
continue
}
if (typeof val === 'function') {
@ -161,13 +165,16 @@ export class TypeDispatcher {
let pattern
let template
if (imps.length) {
for ([pattern, item] of imps) {
let finalIndex
;[finalIndex, template] = pattern.match(types)
if (finalIndex === types.length) {
needItem = false
break
for (const options of [{}, {convert: true}]) {
for ([pattern, item] of imps) {
let finalIndex
;[finalIndex, template] = pattern.match(types, options)
if (finalIndex === types.length) {
needItem = false
break
}
}
if (!needItem) break
}
}
if (needItem && key in this._fallbacks) {
@ -190,7 +197,8 @@ export class TypeDispatcher {
this.resolve._genDepsOf.push([key, types])
let theBehavior = () => undefined
try {
theBehavior = item(DependencyRecorder(this, [], this), template)
theBehavior = item(
DependencyRecorder(this, [], this), matched(template))
} catch {
behave.set(types, item)
return item
@ -199,8 +207,7 @@ export class TypeDispatcher {
}
if (typeof theBehavior === 'function'
&& theBehavior.length
&& Array.isArray(template)
&& template.some(elt => Array.isArray(elt))
&& needsCollection(template)
) {
// have to wrap the behavior to collect the actual arguments
// in the way corresponding to the template

View file

@ -1,7 +1,7 @@
import {Type, Undefined} from './Type.js'
export class TypePattern {
match(typeSequence, position = 0) {
match(typeSequence, options={}) {
throw new Error('Specific TypePatterns must implement match')
}
sampleTypes() {
@ -15,10 +15,16 @@ class MatchTypePattern extends TypePattern {
super()
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
&& 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 [-1, Undefined]
@ -32,15 +38,19 @@ class SequencePattern extends TypePattern {
super()
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 = []
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]
position = newPos
options.position = newPos
matches.push(newMatch)
}
return [position, matches]
return [options.position, matches]
}
sampleTypes() {
return this.patterns.map(pat => pat.sampleTypes()).flat()
@ -62,7 +72,8 @@ export const pattern = patternOrSpec => {
}
class AnyPattern extends TypePattern {
match(typeSequence, position = 0) {
match(typeSequence, options={}) {
const position = options.position ?? 0
return position < typeSequence.length
? [position + 1, typeSequence[position]]
: [-1, Undefined]
@ -77,14 +88,18 @@ class OptionalPattern extends TypePattern {
super()
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 [newPos, newMatch] = this.pattern.match(typeSequence, position)
const [newPos, newMatch] = this.pattern.match(typeSequence, options)
if (newPos >= 0) {
position = newPos
options.position = newPos
matches.push(newMatch)
}
return [position, matches]
return [options.position, matches]
}
sampleTypes() {return []}
equal(other) {
@ -99,12 +114,16 @@ class MultiPattern extends TypePattern {
super()
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 = []
while (true) {
const [newPos, newMatch] = this.pattern.match(typeSequence, position)
if (newPos < 0) return [position, matches]
position = newPos
const [newPos, newMatch] = this.pattern.match(typeSequence, options)
if (newPos < 0) return [options.position, matches]
options.position = newPos
matches.push(newMatch)
}
}
@ -117,3 +136,18 @@ class MultiPattern extends TypePattern {
export const Multiple = item => new MultiPattern(item)
export const alwaysMatches =
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 math from '#nanomath'
import {Number} from '#number/Number.js'
import {NumberT} from '#number/NumberT.js'
import {Returns} from '../Type.js'
import {isPlainFunction} from '../helpers.js'
describe('Core types', () => {
it('creates an object with all of the types', () => {
assert('Number' in math.types)
assert.strictEqual(math.types.Number, Number)
assert('NumberT' in math.types)
assert.strictEqual(math.types.NumberT, NumberT)
assert('Undefined' in math.types)
assert(!('Type' in math.types))
assert(math.types.Undefined.test(undefined))
@ -18,9 +18,9 @@ describe('Core types', () => {
it('supports a typeOf operator', () => {
const tO = math.typeOf
assert.strictEqual(tO(7), Number)
assert.strictEqual(tO(7), NumberT)
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)
})

View file

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

View file

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

View file

@ -5,7 +5,7 @@ describe('generic arithmetic', () => {
it('squares anything', () => {
assert.strictEqual(math.square(7), 49)
assert.strictEqual(
math.square.resolve([math.types.Number]).returns,
math.types.Number)
math.square.resolve([math.types.NumberT]).returns,
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 type from './type.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 {Returns} from '#core/Type.js'
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 {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)
number.also(BooleanT, Returns(NumberT, a => a ? 1 : 0))

View file

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