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 {
|
export class Type {
|
||||||
constructor(f, options = {}) {
|
constructor(f, options = {}) {
|
||||||
this.test = f
|
this.test = f
|
||||||
this.from = options.from ?? []
|
this.from = new Map(options.from ?? [])
|
||||||
this.from.push(this)
|
|
||||||
}
|
}
|
||||||
toString() {
|
toString() {
|
||||||
return this.name || `[Type ${this.test}]`
|
return this.name || `[Type ${this.test}]`
|
||||||
|
|
|
@ -7,17 +7,6 @@ import {
|
||||||
alwaysMatches, matched, needsCollection, Any, Multiple
|
alwaysMatches, matched, needsCollection, Any, Multiple
|
||||||
} from './TypePatterns.js'
|
} 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 {
|
export class TypeDispatcher {
|
||||||
constructor(...specs) {
|
constructor(...specs) {
|
||||||
this._implementations = {} // maps key to list of [pattern, result] pairs
|
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)
|
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) {
|
resolve(key, types) {
|
||||||
if (!(key in this)) {
|
if (!(key in this)) {
|
||||||
throw new ReferenceError(`no method or value for key '${key}'`)
|
throw new ReferenceError(`no method or value for key '${key}'`)
|
||||||
|
@ -200,6 +209,8 @@ export class TypeDispatcher {
|
||||||
theBehavior = item(
|
theBehavior = item(
|
||||||
DependencyRecorder(this, [], this), matched(template))
|
DependencyRecorder(this, [], this), matched(template))
|
||||||
} catch {
|
} 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)
|
behave.set(types, item)
|
||||||
return item
|
return item
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -210,12 +221,15 @@ export class TypeDispatcher {
|
||||||
&& needsCollection(template)
|
&& needsCollection(template)
|
||||||
) {
|
) {
|
||||||
// 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. That may generate
|
||||||
const wrappedBehavior = (...args) => {
|
// more dependencies:
|
||||||
const collectedArgs = collectLike(args, template)
|
this.resolve._genDepsOf.push([key, types])
|
||||||
return theBehavior(...collectedArgs)
|
try {
|
||||||
|
const collectFunction = this._generateCollectFunction(template)
|
||||||
|
theBehavior = (...args) => theBehavior(...collectFunction(args))
|
||||||
|
} finally {
|
||||||
|
this.resolve._genDepsOf.pop()
|
||||||
}
|
}
|
||||||
theBehavior = wrappedBehavior
|
|
||||||
}
|
}
|
||||||
behave.set(types, theBehavior)
|
behave.set(types, theBehavior)
|
||||||
return theBehavior
|
return theBehavior
|
||||||
|
|
|
@ -17,15 +17,12 @@ class MatchTypePattern extends TypePattern {
|
||||||
}
|
}
|
||||||
match(typeSequence, options={}) {
|
match(typeSequence, options={}) {
|
||||||
const position = options.position ?? 0
|
const position = options.position ?? 0
|
||||||
const allowed = options.convert ? this.type.from : [this.type]
|
const actual = typeSequence[position]
|
||||||
if (position < typeSequence.length
|
if (position < typeSequence.length) {
|
||||||
&& allowed.includes(typeSequence[position])
|
if (actual === this.type) return [position + 1, actual]
|
||||||
) {
|
if (options.convert && this.type.from.has(actual)) {
|
||||||
let templateItem = typeSequence[position]
|
return [position + 1, {actual, matched: this.type}]
|
||||||
if (templateItem !== this.type) {
|
|
||||||
templateItem = {actual: templateItem, matched: this.type}
|
|
||||||
}
|
}
|
||||||
return [position + 1, typeSequence[position]]
|
|
||||||
}
|
}
|
||||||
return [-1, Undefined]
|
return [-1, Undefined]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import assert from 'assert'
|
import assert from 'assert'
|
||||||
import {TypeDispatcher} from '../TypeDispatcher.js'
|
import {TypeDispatcher} from '../TypeDispatcher.js'
|
||||||
import {numbers} from '../../numbers.js'
|
import * as numbers from '#number/all.js'
|
||||||
import {generics} from '../../generics.js'
|
import * as generics from '#generic/all.js'
|
||||||
import {onType} from "#core/helpers.js"
|
import {onType} from "#core/helpers.js"
|
||||||
import {Any} from "#core/TypePatterns.js"
|
import {Any} from "#core/TypePatterns.js"
|
||||||
import {Returns} from "#core/Type.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 * as booleans from './boolean/all.js'
|
||||||
import {numbers} from './numbers.js'
|
import * as generics from './generic/all.js'
|
||||||
|
import * as numbers from './number/all.js'
|
||||||
import {TypeDispatcher} from '#core/TypeDispatcher.js'
|
import {TypeDispatcher} from '#core/TypeDispatcher.js'
|
||||||
|
|
||||||
const math = new TypeDispatcher(generics, numbers)
|
const math = new TypeDispatcher(booleans, generics, numbers)
|
||||||
|
|
||||||
export default math
|
export default math
|
||||||
|
|
|
@ -2,5 +2,5 @@ import {Type} from '#core/Type.js'
|
||||||
import {BooleanT} from '#boolean/BooleanT.js'
|
import {BooleanT} from '#boolean/BooleanT.js'
|
||||||
|
|
||||||
export const NumberT = new Type(n => typeof n === 'number', {
|
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 assert from 'assert'
|
||||||
import {NumberT} from '../NumberT.js'
|
import {NumberT} from '../NumberT.js'
|
||||||
|
import {BooleanT} from '#boolean/BooleanT.js'
|
||||||
|
import math from '#nanomath'
|
||||||
|
|
||||||
describe('NumberT Type', () => {
|
describe('NumberT Type', () => {
|
||||||
it('correctly recognizes numbers', () => {
|
it('correctly recognizes numbers', () => {
|
||||||
|
@ -8,4 +10,10 @@ describe('NumberT Type', () => {
|
||||||
assert(NumberT.test(Infinity))
|
assert(NumberT.test(Infinity))
|
||||||
assert(!NumberT.test("3"))
|
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', () => {
|
describe('number type operations', () => {
|
||||||
it('converts to number', () => {
|
it('converts to number', () => {
|
||||||
assert.strictEqual(math.number(2.637), 2.637)
|
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",
|
"#nanomath": "./nanomath.js",
|
||||||
"#boolean/*.js": "./boolean/*.js",
|
"#boolean/*.js": "./boolean/*.js",
|
||||||
"#core/*.js": "./core/*.js",
|
"#core/*.js": "./core/*.js",
|
||||||
|
"#generic/*.js": "./generic/*.js",
|
||||||
"#number/*.js": "./number/*.js"
|
"#number/*.js": "./number/*.js"
|
||||||
},
|
},
|
||||||
"type" : "module"
|
"type" : "module"
|
||||||
|
|
Loading…
Add table
Reference in a new issue