feat: Introduce BooleanT and boolean functions (#17)
All checks were successful
/ test (push) Successful in 17s
All checks were successful
/ test (push) Successful in 17s
This PR adds a boolean section, as well as an isNaN predicate on numbers. In a TypeDispatcher, when BooleanT is present, isNaN returns a BooleanT. However, in a numbers-only TypeDispatcher, it returns 1 or 0 instead. Moreover, when booleans are subsequently added to a numbers-only instance, isNaN properly reconfigures itself to return BooleanT. No predicates that depend on approximate equality testing or a configuration object are implemented in this PR. This PR also implements type matching and dispatching with implicit conversions, and adds an implicit conversion from BooleanT to NumberT. Reviewed-on: #17 Co-authored-by: Glen Whitney <glen@studioinfinity.org> Co-committed-by: Glen Whitney <glen@studioinfinity.org>
This commit is contained in:
parent
14011984a0
commit
27fa4b0193
31 changed files with 432 additions and 142 deletions
|
@ -18,4 +18,7 @@
|
|||
devDependencies: {
|
||||
mocha: '^11.1.0',
|
||||
},
|
||||
dependencies: {
|
||||
'array-keyed-map': '^2.1.3',
|
||||
},
|
||||
}
|
||||
|
|
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
|
@ -7,6 +7,10 @@ settings:
|
|||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
array-keyed-map:
|
||||
specifier: ^2.1.3
|
||||
version: 2.1.3
|
||||
devDependencies:
|
||||
mocha:
|
||||
specifier: ^11.1.0
|
||||
|
@ -49,6 +53,9 @@ packages:
|
|||
argparse@2.0.1:
|
||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||
|
||||
array-keyed-map@2.1.3:
|
||||
resolution: {integrity: sha512-JIUwuFakO+jHjxyp4YgSiKXSZeC0U+R1jR94bXWBcVlFRBycqXlb+kH9JHxBGcxnVuSqx5bnn0Qz9xtSeKOjiA==}
|
||||
|
||||
balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
|
||||
|
@ -398,6 +405,8 @@ snapshots:
|
|||
|
||||
argparse@2.0.1: {}
|
||||
|
||||
array-keyed-map@2.1.3: {}
|
||||
|
||||
balanced-match@1.0.2: {}
|
||||
|
||||
binary-extensions@2.3.0: {}
|
||||
|
|
13
src/__test__/numbers.spec.js
Normal file
13
src/__test__/numbers.spec.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
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)
|
||||
})
|
||||
it('uses 1 and 0 instead of true and false', () => {
|
||||
assert.strictEqual(math.isnan(-16.5), 0)
|
||||
assert.strictEqual(math.isnan(NaN), 1)
|
||||
})
|
||||
})
|
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')
|
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)
|
||||
})
|
||||
})
|
25
src/boolean/__test__/type.spec.js
Normal file
25
src/boolean/__test__/type.spec.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
import assert from 'assert'
|
||||
import math from '#nanomath'
|
||||
import {Type} from '#core/Type.js'
|
||||
|
||||
const boolean = math.boolean
|
||||
|
||||
describe('boolean type functions', () => {
|
||||
it('properly converts to boolean', () => {
|
||||
assert.strictEqual(boolean(false), false)
|
||||
assert.strictEqual(boolean(true), true)
|
||||
assert.strictEqual(boolean(0), false)
|
||||
assert.strictEqual(boolean(-0), false)
|
||||
assert.strictEqual(boolean(NaN), false)
|
||||
assert.strictEqual(boolean(Infinity), true)
|
||||
assert.strictEqual(boolean(1e-30), true)
|
||||
assert.strictEqual(boolean(undefined), false)
|
||||
assert.strictEqual(boolean(), false)
|
||||
assert.strictEqual(boolean(math.types.NumberT), true)
|
||||
})
|
||||
it('converts any type to boolean', () => {
|
||||
for (const T in math.types) {
|
||||
if (T instanceof Type) assert(boolean.resolve([T]))
|
||||
}
|
||||
})
|
||||
})
|
2
src/boolean/all.js
Normal file
2
src/boolean/all.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * as typeDefinition from './BooleanT.js'
|
||||
export * as type from './type.js'
|
14
src/boolean/type.js
Normal file
14
src/boolean/type.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
import {BooleanT} from './BooleanT.js'
|
||||
import {onType} from '#core/helpers.js'
|
||||
import {Returns, Type, TypeOfTypes, Undefined} from '#core/Type.js'
|
||||
import {NumberT} from '#number/NumberT.js'
|
||||
|
||||
const bool = f => Returns(BooleanT, f)
|
||||
|
||||
export const boolean = onType(
|
||||
BooleanT, bool(p => p),
|
||||
NumberT, bool(a => !!a),
|
||||
TypeOfTypes, bool(() => true),
|
||||
Undefined, bool(() => false),
|
||||
[], bool(() => false)
|
||||
)
|
|
@ -1,10 +1,10 @@
|
|||
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 ?? {patterns: []} // mock empty Implementations
|
||||
}
|
||||
toString() {
|
||||
return this.name || `[Type ${this.test}]`
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,8 +13,8 @@ export const TypeOfTypes = new Type(t => t instanceof Type)
|
|||
|
||||
export const Returns = (type, f) => (f.returns = type, f)
|
||||
|
||||
export const typeOf = Returns(TypeOfTypes, item => {
|
||||
for (const type of Object.values(typeObject)) {
|
||||
export const whichType = typs => Returns(TypeOfTypes, item => {
|
||||
for (const type of Object.values(typs)) {
|
||||
if (!(type instanceof Type)) continue
|
||||
if (type.test(item)) return type
|
||||
}
|
||||
|
@ -27,10 +27,12 @@ export const typeOf = Returns(TypeOfTypes, item => {
|
|||
throw new TypeError(errorMsg)
|
||||
})
|
||||
|
||||
export const typeOf = math => whichType(math.types)
|
||||
|
||||
// bootstrapping order matters, but order of exports in a module isn't
|
||||
// simply the order that the items are listed in the module. So we make
|
||||
// an explicitly ordered export of implementations for this sake:
|
||||
|
||||
export const bootstrapTypes = {
|
||||
types, Type, Undefined, TypeOfTypes, typeOf
|
||||
Type, Undefined, TypeOfTypes, typeOf
|
||||
}
|
||||
|
|
|
@ -1,27 +1,20 @@
|
|||
import {typeOf, Type, bootstrapTypes} from './Type.js'
|
||||
import ArrayKeyedMap from 'array-keyed-map'
|
||||
|
||||
import {
|
||||
Implementations, isPlainFunction, isPlainObject, onType
|
||||
} from './helpers.js'
|
||||
import {alwaysMatches, Any, Multiple} from './TypePatterns.js'
|
||||
|
||||
// helper that organizes a list into the same chunks as a pattern is
|
||||
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
|
||||
}
|
||||
import {bootstrapTypes, Returns, whichType, Type} from './Type.js'
|
||||
import {matched, needsCollection, Passthru} from './TypePatterns.js'
|
||||
|
||||
export class TypeDispatcher {
|
||||
constructor(...specs) {
|
||||
this._types = {} // stores all the types registered in this dispatcher
|
||||
this._implementations = {} // maps key to list of [pattern, result] pairs
|
||||
this._dependencies = {} // maps key to a map from type vectors to...
|
||||
// ...a set of [key, types] that depend on it.
|
||||
this._behaviors = {} // maps key to a map from type vectors to results
|
||||
this._fallbacks = {} // maps key to a catchall result
|
||||
this.merge({types: () => this._types})
|
||||
this.merge(bootstrapTypes) // bootstrap the instance
|
||||
for (const spec of specs) this.merge(spec)
|
||||
}
|
||||
|
@ -39,37 +32,57 @@ export class TypeDispatcher {
|
|||
for (const key in spec) {
|
||||
let val = spec[key]
|
||||
if (val instanceof Type) {
|
||||
// TODO: Need to wipe out any dependencies on types[key]!
|
||||
// The design here is that we have set up the `types` property to
|
||||
// be watched like any other, so the following assignment will
|
||||
// cause any behaviors that depend on the type named `key` in the
|
||||
// list of types to be cleared out automagically. Seems to work
|
||||
// so far.
|
||||
this.types[key] = val
|
||||
val.name = key
|
||||
continue
|
||||
}
|
||||
if (typeof val === 'function') {
|
||||
val = onType(Multiple(Any), val)
|
||||
val = onType(Passthru, val)
|
||||
}
|
||||
if (val instanceof Implementations) {
|
||||
if (!(key in this)) {
|
||||
// need to set up the item
|
||||
// Need to "bootstrap" the item:
|
||||
// We initially define it with a temporary getter, only
|
||||
// because we don't know whether ultimately it will produce
|
||||
// a function or a non-callable value, and the "permanent"
|
||||
// getter we want depends on which it turns out to be. This
|
||||
// situation means it's not supported to replace a key
|
||||
// corresponding to a method, after it's been fetched once,
|
||||
// with a definition that produces a non-callable value;
|
||||
// the TypeDispatcher will go on returning a function, and
|
||||
// even if you call that function, it will throw an error
|
||||
// when it tries to call the non-callable value.
|
||||
// Conversely, if you try to replace a key corresponding
|
||||
// to a non-callable value, after it's been fetched once,
|
||||
// with a function, it will work, but it will not be able to
|
||||
// perform type dispatch on that function.
|
||||
Object.defineProperty(this, key, {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
get: () => {
|
||||
let tryValue
|
||||
let tryTypes = []
|
||||
try {
|
||||
tryValue = this.resolve(key, [])
|
||||
} catch {
|
||||
// Has no value for the empty type list, so
|
||||
// find a type list it will have a value for
|
||||
tryTypes =
|
||||
this._implementations[key][0][0].sampleTypes()
|
||||
tryValue = this.resolve(key, tryTypes)
|
||||
// Has no value for the empty type list, so therefore
|
||||
// it must be a method, as there is no way to supply
|
||||
// any types for a non-function value. Hence, we can
|
||||
// just make tryValue any plain function, since it is
|
||||
// never actually used, just its type analyzed.
|
||||
tryValue = () => undefined
|
||||
}
|
||||
// Redefine the property according to what sort of
|
||||
// entity it is:
|
||||
if (isPlainFunction(tryValue)) {
|
||||
const thisTypeOf = whichType(this.types)
|
||||
// the usual case: a method of the dispatcher
|
||||
const standard = (...args) => {
|
||||
const types = args.map(typeOf)
|
||||
const types = args.map(thisTypeOf)
|
||||
return this.resolve(key, types)(...args)
|
||||
}
|
||||
standard.resolve = (types) => this.resolve(key, types)
|
||||
|
@ -81,7 +94,7 @@ export class TypeDispatcher {
|
|||
})
|
||||
return standard
|
||||
}
|
||||
if (tryTypes.length) tryValue = undefined
|
||||
|
||||
if (typeof tryValue === 'object') {
|
||||
if (!('resolve' in tryValue)) {
|
||||
tryValue.resolve = types => this.resolve(key, types)
|
||||
|
@ -99,6 +112,7 @@ export class TypeDispatcher {
|
|||
})
|
||||
return get()
|
||||
}
|
||||
|
||||
Object.defineProperty(this, key, {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
|
@ -107,13 +121,16 @@ export class TypeDispatcher {
|
|||
return tryValue
|
||||
}
|
||||
})
|
||||
|
||||
// Finally, initialize the other data for this key:
|
||||
this._implementations[key] = []
|
||||
this._behaviors[key] = new Map()
|
||||
this._dependencies[key] = new Map()
|
||||
this._behaviors[key] = new ArrayKeyedMap()
|
||||
this._dependencies[key] = new ArrayKeyedMap()
|
||||
}
|
||||
|
||||
// Now add all of the patterns of this implementation:
|
||||
for (const [pattern, result] of val.patterns) {
|
||||
if (alwaysMatches(pattern)) {
|
||||
if (pattern === Passthru) {
|
||||
if (key in this._fallbacks) this._disengageFallback(key)
|
||||
this._fallbacks[key] = result
|
||||
} else {
|
||||
|
@ -135,7 +152,7 @@ export class TypeDispatcher {
|
|||
}
|
||||
|
||||
// install value as a catchall value
|
||||
this.merge({[key]: onType(Multiple(Any), val)})
|
||||
this.merge({[key]: onType(Passthru, val)})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -146,6 +163,29 @@ 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
|
||||
let convert = elt.convertor
|
||||
if (!convert.returns) { // it's a factory that produces convert
|
||||
convert = convert(this, elt.actual)
|
||||
}
|
||||
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}'`)
|
||||
|
@ -154,64 +194,84 @@ export class TypeDispatcher {
|
|||
if (this.resolve._genDepsOf?.length) this._addToDeps(key, types)
|
||||
|
||||
const behave = this._behaviors[key]
|
||||
// Return the cached resolution if it's there
|
||||
if (behave.has(types)) return behave.get(types)
|
||||
|
||||
// Otherwise, perform the resolution and cache the result
|
||||
const imps = this._implementations[key]
|
||||
let needItem = true
|
||||
let item
|
||||
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}]) {
|
||||
let pattern
|
||||
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) {
|
||||
needItem = false
|
||||
item = this._fallbacks[key]
|
||||
template = [types]
|
||||
template = types
|
||||
}
|
||||
if (needItem) {
|
||||
throw new TypeError(`no matching definition of '${key}' on '${types}'`)
|
||||
}
|
||||
if (!isPlainFunction(item) || 'returns' in item) {
|
||||
// If this key is producing a non-function value, we're done
|
||||
if (!isPlainFunction(item)) {
|
||||
behave.set(types, item)
|
||||
return item
|
||||
}
|
||||
// item is a Factory. We have to use it to build the behavior
|
||||
|
||||
// item is a function, either a direct behavior or
|
||||
// a factory. We have to use it to build the final behavior
|
||||
// First set up to record dependencies
|
||||
if (!('_genDepsOf' in this.resolve)) {
|
||||
this.resolve._genDepsOf = []
|
||||
}
|
||||
this.resolve._genDepsOf.push([key, types])
|
||||
|
||||
let theBehavior = () => undefined
|
||||
try {
|
||||
theBehavior = item(DependencyRecorder(this, [], this), template)
|
||||
} catch {
|
||||
behave.set(types, item)
|
||||
return item
|
||||
} finally {
|
||||
this.resolve._genDepsOf.pop()
|
||||
}
|
||||
if (typeof theBehavior === 'function'
|
||||
&& theBehavior.length
|
||||
&& Array.isArray(template)
|
||||
&& template.some(elt => Array.isArray(elt))
|
||||
) {
|
||||
// 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)
|
||||
this.resolve._genDepsOf.push([key, types]) // Important: make sure
|
||||
// not to return without popping _genDepsOf
|
||||
if (!('returns' in item)) {
|
||||
// looks like a factory
|
||||
try {
|
||||
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:
|
||||
theBehavior = item
|
||||
}
|
||||
} else theBehavior = item
|
||||
|
||||
let finalBehavior = theBehavior
|
||||
if (typeof theBehavior === 'function') {
|
||||
const returning = theBehavior.returns
|
||||
if (!returning) {
|
||||
throw new TypeError(
|
||||
`No return type specified for ${key} on ${types}`)
|
||||
}
|
||||
if (theBehavior.length && needsCollection(template)) {
|
||||
// have to wrap the behavior to collect the actual arguments
|
||||
// in the way corresponding to the template. Generating that
|
||||
// argument transformer may generate more dependencies.
|
||||
const morph = this._generateCollectFunction(template)
|
||||
finalBehavior =
|
||||
Returns(returning, (...args) => theBehavior(...morph(args)))
|
||||
}
|
||||
theBehavior = wrappedBehavior
|
||||
}
|
||||
behave.set(types, theBehavior)
|
||||
return theBehavior
|
||||
|
||||
this.resolve._genDepsOf.pop() // OK, now it's safe to return
|
||||
behave.set(types, finalBehavior)
|
||||
return finalBehavior
|
||||
}
|
||||
|
||||
// Method called to invalidate a set of behaviors
|
||||
|
@ -267,7 +327,11 @@ const DependencyRecorder = (object, path, repo) => new Proxy(object, {
|
|||
get(target, prop, receiver) {
|
||||
const result = Reflect.get(target, prop, receiver)
|
||||
// pass resolve calls through, since we record dependencies therein:
|
||||
if (prop === 'resolve' || 'isDispatcher' in result) return result
|
||||
if (prop === 'resolve'
|
||||
|| (typeof result === 'function' && 'isDispatcher' in result)
|
||||
) {
|
||||
return result
|
||||
}
|
||||
|
||||
// OK, it's not a method on a TypeDispatcher, it's some other kind of
|
||||
// value. So first record the dependency on prop at this path:
|
||||
|
@ -278,8 +342,9 @@ const DependencyRecorder = (object, path, repo) => new Proxy(object, {
|
|||
repo._addToDeps(key, subpath)
|
||||
// Now, if the result is an object, we may need to record further
|
||||
// dependencies on its properties (e.g. math.config.predictable)
|
||||
// So proxy the return value:
|
||||
if (typeof result === 'object') {
|
||||
// So proxy the return value, except for types, which must maintain
|
||||
// strict referential identity:
|
||||
if (typeof result === 'object' && !(result instanceof Type)) {
|
||||
return DependencyRecorder(result, newPath, repo)
|
||||
} else return result
|
||||
}
|
||||
|
@ -292,7 +357,7 @@ const DependencyWatcher = (object, path, repo) => new Proxy(object, {
|
|||
// First see if this setting has any dependencies:
|
||||
const newPath = path.slice()
|
||||
newPath.push(prop)
|
||||
const key = newPath.unshift()
|
||||
const key = newPath.shift()
|
||||
const depSet = repo._dependencies[key]?.get(newPath)
|
||||
if (depSet?.size) {
|
||||
// It does. So if we are changing it, invalidate them:
|
||||
|
@ -305,7 +370,7 @@ const DependencyWatcher = (object, path, repo) => new Proxy(object, {
|
|||
get(target, prop, receiver) {
|
||||
// Only thing we need to do is push the watching down
|
||||
const result = Reflect.get(target, prop, receiver)
|
||||
if (typeof result === 'object') {
|
||||
if (typeof result === 'object' && !(result instanceof Type)) {
|
||||
const newPath = path.slice()
|
||||
newPath.push(prop)
|
||||
return DependencyWatcher(result, newPath, repo)
|
||||
|
|
|
@ -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,11 +15,19 @@ class MatchTypePattern extends TypePattern {
|
|||
super()
|
||||
this.type = typeToMatch
|
||||
}
|
||||
match(typeSequence, position = 0) {
|
||||
if (position < typeSequence.length
|
||||
&& typeSequence[position] === this.type
|
||||
) {
|
||||
return [position + 1, typeSequence[position]]
|
||||
match(typeSequence, options={}) {
|
||||
const position = options.position ?? 0
|
||||
const actual = typeSequence[position]
|
||||
if (position < typeSequence.length) {
|
||||
if (actual === this.type) return [position + 1, actual]
|
||||
if (options.convert) {
|
||||
for (const [pattern, convertor] of this.type.from.patterns) {
|
||||
const [pos] = pattern.match([actual])
|
||||
if (pos === 1) {
|
||||
return [position + 1, {actual, convertor, matched: this.type}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return [-1, Undefined]
|
||||
}
|
||||
|
@ -32,15 +40,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 +74,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 +90,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 +116,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)
|
||||
}
|
||||
}
|
||||
|
@ -115,5 +136,30 @@ class MultiPattern extends TypePattern {
|
|||
}
|
||||
|
||||
export const Multiple = item => new MultiPattern(item)
|
||||
export const alwaysMatches =
|
||||
pat => pat instanceof MultiPattern && pat.pattern instanceof AnyPattern
|
||||
|
||||
// Like Multiple(Any) except leaves the argument list alone; it doesn't
|
||||
// chunk it into a single Array of all arguments
|
||||
class PassthruPattern extends TypePattern {
|
||||
match(typeSequence, options={}) {
|
||||
const position = options.position ?? 0
|
||||
return [typeSequence.length, typeSequence.slice(position)]
|
||||
}
|
||||
sampleTypes() {return []}
|
||||
}
|
||||
|
||||
export const Passthru = new PassthruPattern()
|
||||
|
||||
// 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 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)
|
||||
})
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import assert from 'assert'
|
||||
import {TypeDispatcher} from '../TypeDispatcher.js'
|
||||
import {numbers} from '../../numbers.js'
|
||||
import {generics} from '../../generics.js'
|
||||
import * as booleans from '#boolean/all.js'
|
||||
import * as generics from '#generic/all.js'
|
||||
import * as numbers from '#number/all.js'
|
||||
import {onType} from "#core/helpers.js"
|
||||
import {Any} from "#core/TypePatterns.js"
|
||||
import {Returns} from "#core/Type.js"
|
||||
|
@ -13,8 +14,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 +28,22 @@ 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)
|
||||
assert.strictEqual(incremental.isnan(NaN), 1)
|
||||
incremental.merge(booleans)
|
||||
assert.strictEqual(incremental.boolean(undefined), false)
|
||||
assert.strictEqual(incremental.isnan(NaN), true)
|
||||
})
|
||||
it('changes methods when their dependencies change', () => {
|
||||
const gnmath = new TypeDispatcher(generics, numbers)
|
||||
|
@ -47,4 +52,15 @@ describe('TypeDispatcher', () => {
|
|||
gnmath.merge({multiply: plain((a,b) => Math.floor(a) * b)})
|
||||
assert.strictEqual(gnmath.square(-2.5), 7.5)
|
||||
})
|
||||
it('detects dependencies on conversion operations', () => {
|
||||
const bgn = new TypeDispatcher(booleans, generics, numbers)
|
||||
const {BooleanT, NumberT} = bgn.types
|
||||
assert(!bgn._behaviors.negate.has([BooleanT]))
|
||||
assert.strictEqual(bgn.negate(true), -1)
|
||||
assert(bgn._behaviors.negate.has([BooleanT]))
|
||||
const deps = bgn._dependencies.negate
|
||||
bgn.merge({number: onType([BooleanT], Returns(NumberT, b => b ? 2 : 0))})
|
||||
assert(!bgn._behaviors.negate.has([BooleanT]))
|
||||
assert.strictEqual(bgn.negate(true), -2)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import assert from 'assert'
|
||||
import {pattern, Any, Multiple, Optional} from '../TypePatterns.js'
|
||||
import {
|
||||
pattern, Any, Multiple, Optional, needsCollection
|
||||
} from '../TypePatterns.js'
|
||||
import {Undefined, TypeOfTypes} from '../Type.js'
|
||||
|
||||
describe('Type patterns', () => {
|
||||
|
@ -93,4 +95,10 @@ describe('Type patterns', () => {
|
|||
whyNot.sampleTypes(), [Undefined, TypeOfTypes])
|
||||
assert(whyNot.equal(whyNot))
|
||||
})
|
||||
it('determines whether a template needs a collection function', () => {
|
||||
assert(!needsCollection([Undefined]))
|
||||
assert(needsCollection([Undefined, [Undefined, Undefined]]))
|
||||
assert(needsCollection(
|
||||
[Undefined, {actual: Undefined, matched: TypeOfTypes}]))
|
||||
})
|
||||
})
|
||||
|
|
|
@ -9,8 +9,8 @@ describe('Core helpers', () => {
|
|||
it('defines what Implementations are', () => {
|
||||
const imps = onType(Undefined, 7, [TypeOfTypes, Undefined], -3)
|
||||
assert(imps instanceof Implementations)
|
||||
assert(imps.patterns instanceof Map)
|
||||
assert(imps.patterns.keys().every(k => k instanceof TypePattern))
|
||||
assert(imps.patterns instanceof Array)
|
||||
assert(imps.patterns.every(([k]) => k instanceof TypePattern))
|
||||
})
|
||||
|
||||
it('detects plain objects', () => {
|
||||
|
|
|
@ -1,13 +1,18 @@
|
|||
import {Type} from './Type.js'
|
||||
import {pattern} from './TypePatterns.js'
|
||||
|
||||
export class Implementations {
|
||||
constructor(imps) {
|
||||
this.patterns = new Map()
|
||||
this.patterns = []
|
||||
this._add(imps)
|
||||
}
|
||||
_add(imps) {
|
||||
for (let i = 0; i < imps.length; ++i) {
|
||||
this.patterns.set(pattern(imps[i]), imps[++i])
|
||||
this.patterns.push([pattern(imps[i]), imps[++i]])
|
||||
}
|
||||
}
|
||||
also(...imps) {
|
||||
this._add(imps)
|
||||
}
|
||||
}
|
||||
|
||||
export const onType = (...imps) => new Implementations(imps)
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
import {Type} from '#core/Type.js'
|
||||
|
||||
export const Number = new Type(n => typeof n === 'number')
|
7
src/number/NumberT.js
Normal file
7
src/number/NumberT.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import {Type} from '#core/Type.js'
|
||||
import {onType} from '#core/helpers.js'
|
||||
import {BooleanT} from '#boolean/BooleanT.js'
|
||||
|
||||
export const NumberT = new Type(n => typeof n === 'number', {
|
||||
from: onType(BooleanT, math => math.number.resolve([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"))
|
||||
})
|
||||
})
|
26
src/number/__test__/NumberT.spec.js
Normal file
26
src/number/__test__/NumberT.spec.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
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', () => {
|
||||
assert(NumberT.test(3))
|
||||
assert(NumberT.test(NaN))
|
||||
assert(NumberT.test(Infinity))
|
||||
assert(!NumberT.test("3"))
|
||||
})
|
||||
|
||||
it('can convert from BooleanT to NumberT', () => {
|
||||
const convertImps = NumberT.from
|
||||
let cnvBtoN
|
||||
for (const [pattern, convFactory] of convertImps.patterns) {
|
||||
if (pattern.match([BooleanT])) {
|
||||
cnvBtoN = convFactory(math)
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.strictEqual(cnvBtoN(true), 1)
|
||||
assert.strictEqual(cnvBtoN(false), 0)
|
||||
})
|
||||
})
|
|
@ -4,5 +4,9 @@ import math from '#nanomath'
|
|||
describe('number type operations', () => {
|
||||
it('converts to number', () => {
|
||||
assert.strictEqual(math.number(2.637), 2.637)
|
||||
assert(isNaN(math.number(NaN)))
|
||||
assert.strictEqual(math.number(true), 1)
|
||||
assert.strictEqual(math.number(false), 0)
|
||||
assert.strictEqual(math.number(), 0)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -5,4 +5,9 @@ describe('number utilities', () => {
|
|||
it('clones a number', () => {
|
||||
assert.strictEqual(math.clone(2.637), 2.637)
|
||||
})
|
||||
it('tests if a number is NaN', () => {
|
||||
assert.strictEqual(math.isnan(NaN), true)
|
||||
assert.strictEqual(math.isnan(Infinity), false)
|
||||
assert.strictEqual(math.isnan(43), false)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
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))
|
||||
|
||||
export const boolnum = Returns(NumberT, p => p ? 1 : 0)
|
||||
|
|
|
@ -1,4 +1,12 @@
|
|||
import {plain} from './helpers.js'
|
||||
import {plain, boolnum} from './helpers.js'
|
||||
import {BooleanT} from '#boolean/BooleanT.js'
|
||||
import {Returns} from '#core/Type.js'
|
||||
import {NumberT} from '#number/NumberT.js'
|
||||
|
||||
const num = f => Returns(NumberT, f)
|
||||
|
||||
// Not much to do so far when there is only one type
|
||||
export const number = plain(a => a)
|
||||
number.also(
|
||||
BooleanT, boolnum,
|
||||
[], num(() => 0)
|
||||
)
|
||||
|
|
|
@ -1,3 +1,12 @@
|
|||
import {plain} from './helpers.js'
|
||||
import {plain, boolnum} from './helpers.js'
|
||||
import {NumberT} from './NumberT.js'
|
||||
|
||||
import {Returns} from '#core/Type.js'
|
||||
import {onType} from '#core/helpers.js'
|
||||
|
||||
export const clone = plain(a => a)
|
||||
export const isnan = onType(NumberT, math => {
|
||||
const {BooleanT} = math.types
|
||||
if (BooleanT) return Returns(BooleanT, a => isNaN(a))
|
||||
return Returns(NumberT, a => boolnum(isNaN(a)))
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
{
|
||||
"imports" : {
|
||||
"#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