feat: implicit convert BooleanT to NumberT
All checks were successful
/ test (pull_request) Successful in 19s

Also adds implicit conversion configuration option to Type constructor,
  and institutes a numbers-only bundle, and supports argument matching
  with implicit conversions.

  Still need to test that a behavior that invokes implicit conversion ends up
  with the conversion operation as a dependency (and so regenerates itself if
  the conversion changes).
This commit is contained in:
Glen Whitney 2025-04-10 22:47:30 -07:00
parent 5bee93dbb3
commit bfc64f3789
14 changed files with 95 additions and 34 deletions

View file

@ -0,0 +1,9 @@
import assert from 'assert'
import math from '../numbers.js'
describe('the numbers-only bundle', () => {
it('works like regular on number-only functions', () => {
assert.strictEqual(math.quotient(5, 3), 1)
assert.strictEqual(math.square(-3), 9)
})
})

View file

@ -0,0 +1,24 @@
import assert from 'assert'
import {BooleanT} from '../BooleanT.js'
import math from '#nanomath'
describe('BooleanT Type', () => {
it('correctly recognizes booleans', () => {
assert(BooleanT.test(true))
assert(BooleanT.test(false))
assert(!BooleanT.test(null))
assert(!BooleanT.test(1))
})
it('autoconverts to number type', () => {
assert.strictEqual(math.abs(false), 0)
assert.strictEqual(math.absquare(true), 1)
assert.strictEqual(math.add(true, true), 2)
assert.strictEqual(math.divide(false, true), 0)
assert.strictEqual(math.cbrt(true), 1)
assert.strictEqual(math.invert(true), 1)
assert.strictEqual(math.multiply(false, false), 0)
assert.strictEqual(math.negate(false), -0)
assert.strictEqual(math.subtract(false, true), -1)
assert.strictEqual(math.quotient(true, true), 1)
})
})

1
src/boolean/all.js Normal file
View file

@ -0,0 +1 @@
export * as typeDefinition from './BooleanT.js'

View file

@ -5,8 +5,7 @@ export const types = () => typeObject
export class Type {
constructor(f, options = {}) {
this.test = f
this.from = options.from ?? []
this.from.push(this)
this.from = new Map(options.from ?? [])
}
toString() {
return this.name || `[Type ${this.test}]`

View file

@ -7,17 +7,6 @@ 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) {
if (Array.isArray(elt)) result.push(collectLike(list, elt, state))
else result.push(list[state.pos++])
}
return result
}
export class TypeDispatcher {
constructor(...specs) {
this._implementations = {} // maps key to list of [pattern, result] pairs
@ -150,6 +139,26 @@ export class TypeDispatcher {
for (const pair of this.resolve._genDepsOf) depSet.add(pair)
}
// produces and returns a function that takes a list of arguments and
// transforms them per the given template, to massage them into the form
// expected by a behavior associated with the TypePattern that produced
// the template.
_generateCollectFunction(template, state = {pos: 0}) {
const extractors = []
for (const elt of template) {
if (Array.isArray(elt)) {
extractors.push(this._generateCollectFunction(elt, state))
} else {
const from = state.pos++
if ('actual' in elt) { // incorporate conversion
const convert = elt.matched.from.get(elt.actual)(this)
extractors.push(args => convert(args[from]))
} else extractors.push(args => args[from])
}
}
return args => extractors.map(f => f(args))
}
resolve(key, types) {
if (!(key in this)) {
throw new ReferenceError(`no method or value for key '${key}'`)
@ -200,6 +209,8 @@ export class TypeDispatcher {
theBehavior = item(
DependencyRecorder(this, [], this), matched(template))
} catch {
// Oops, didn't work as a factory, so guess we were wrong.
// Just make it the direct value for this key on these types:
behave.set(types, item)
return item
} finally {
@ -210,12 +221,15 @@ export class TypeDispatcher {
&& needsCollection(template)
) {
// have to wrap the behavior to collect the actual arguments
// in the way corresponding to the template
const wrappedBehavior = (...args) => {
const collectedArgs = collectLike(args, template)
return theBehavior(...collectedArgs)
// in the way corresponding to the template. That may generate
// more dependencies:
this.resolve._genDepsOf.push([key, types])
try {
const collectFunction = this._generateCollectFunction(template)
theBehavior = (...args) => theBehavior(...collectFunction(args))
} finally {
this.resolve._genDepsOf.pop()
}
theBehavior = wrappedBehavior
}
behave.set(types, theBehavior)
return theBehavior

View file

@ -17,15 +17,12 @@ class MatchTypePattern extends TypePattern {
}
match(typeSequence, options={}) {
const position = options.position ?? 0
const allowed = options.convert ? this.type.from : [this.type]
if (position < typeSequence.length
&& allowed.includes(typeSequence[position])
) {
let templateItem = typeSequence[position]
if (templateItem !== this.type) {
templateItem = {actual: templateItem, matched: this.type}
const actual = typeSequence[position]
if (position < typeSequence.length) {
if (actual === this.type) return [position + 1, actual]
if (options.convert && this.type.from.has(actual)) {
return [position + 1, {actual, matched: this.type}]
}
return [position + 1, typeSequence[position]]
}
return [-1, Undefined]
}

View file

@ -1,7 +1,7 @@
import assert from 'assert'
import {TypeDispatcher} from '../TypeDispatcher.js'
import {numbers} from '../../numbers.js'
import {generics} from '../../generics.js'
import * as numbers from '#number/all.js'
import * as generics from '#generic/all.js'
import {onType} from "#core/helpers.js"
import {Any} from "#core/TypePatterns.js"
import {Returns} from "#core/Type.js"

View file

@ -1 +0,0 @@
export * as generics from './generic/all.js'

View file

@ -1,7 +1,8 @@
import {generics} from './generics.js'
import {numbers} from './numbers.js'
import * as booleans from './boolean/all.js'
import * as generics from './generic/all.js'
import * as numbers from './number/all.js'
import {TypeDispatcher} from '#core/TypeDispatcher.js'
const math = new TypeDispatcher(generics, numbers)
const math = new TypeDispatcher(booleans, generics, numbers)
export default math

View file

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

View file

@ -1,5 +1,7 @@
import assert from 'assert'
import {NumberT} from '../NumberT.js'
import {BooleanT} from '#boolean/BooleanT.js'
import math from '#nanomath'
describe('NumberT Type', () => {
it('correctly recognizes numbers', () => {
@ -8,4 +10,10 @@ describe('NumberT Type', () => {
assert(NumberT.test(Infinity))
assert(!NumberT.test("3"))
})
it('can convert from BooleanT to NumberT', () => {
const cnvBtoN = NumberT.from.get(BooleanT)(math)
assert.strictEqual(cnvBtoN(true), 1)
assert.strictEqual(cnvBtoN(false), 0)
})
})

View file

@ -4,5 +4,7 @@ import math from '#nanomath'
describe('number type operations', () => {
it('converts to number', () => {
assert.strictEqual(math.number(2.637), 2.637)
assert.strictEqual(math.number(true), 1)
assert.strictEqual(math.number(false), 0)
})
})

View file

@ -1 +1,7 @@
export * as numbers from './number/all.js'
import * as numbers from './number/all.js'
import * as generics from './generic/all.js'
import {TypeDispatcher} from '#core/TypeDispatcher.js'
const math = new TypeDispatcher(numbers, generics)
export default math

View file

@ -3,6 +3,7 @@
"#nanomath": "./nanomath.js",
"#boolean/*.js": "./boolean/*.js",
"#core/*.js": "./core/*.js",
"#generic/*.js": "./generic/*.js",
"#number/*.js": "./number/*.js"
},
"type" : "module"