refactor: prepare for boolean functions
All checks were successful
/ test (pull_request) Successful in 19s
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:
parent
14011984a0
commit
5bee93dbb3
16 changed files with 123 additions and 62 deletions
3
src/boolean/BooleanT.js
Normal file
3
src/boolean/BooleanT.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import {Type} from '#core/Type.js'
|
||||||
|
|
||||||
|
export const BooleanT = new Type(n => typeof n === 'boolean')
|
|
@ -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}]`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -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
6
src/number/NumberT.js
Normal 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]
|
||||||
|
})
|
|
@ -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"))
|
|
||||||
})
|
|
||||||
})
|
|
11
src/number/__test__/NumberT.spec.js
Normal file
11
src/number/__test__/NumberT.spec.js
Normal 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"))
|
||||||
|
})
|
||||||
|
})
|
|
@ -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'
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue