feat: implicit convert BooleanT to NumberT
All checks were successful
/ test (pull_request) Successful in 19s
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:
parent
5bee93dbb3
commit
bfc64f3789
14 changed files with 95 additions and 34 deletions
9
src/__test__/numbers.spec.js
Normal file
9
src/__test__/numbers.spec.js
Normal 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)
|
||||
})
|
||||
})
|
24
src/boolean/__test__/BooleanT.spec.js
Normal file
24
src/boolean/__test__/BooleanT.spec.js
Normal 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
1
src/boolean/all.js
Normal file
|
@ -0,0 +1 @@
|
|||
export * as typeDefinition from './BooleanT.js'
|
|
@ -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}]`
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
export * as generics from './generic/all.js'
|
|
@ -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
|
||||
|
|
|
@ -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])]],
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Add table
Reference in a new issue