Adds type constants zero and one, and allows you to obtain them directly from a type object. This facility creates a behavior with a parametric type: the type of `math.zero(T)` where `T` is a Type object (i.e., has type `TypeOfTypes`) depends not just on that type TypeOfTypes, but instead on the _value_ of the argument `T`. Since nanomath is not (yet?) equipped to handle typing such a method, we just set its return type to a new constant NotAType that (hopefully) does not work with the rest of the type system. Also allows you to compute `zero` and `one` from an example value, rather than from the type object itself. Adds utility function `isZero` to test if a value is zero. As usual so far, the additions uncovered some remaining bugs, which this PR fixes. For example, there was a problem in that resolution of the `one` method was failing because the `Any` pattern was blocking matching of the `TypeOfTypes` pattern. Although we may eventually need to sort the patterns for a given method to maintain a reasonable matching order, for now the solution was just to move the two patterns into the same source file and explicitly order them. (With the way onType and Implementations are currently implemented, the proper ordering is more general to more specific, i.e. later implementations supersede earlier ones. Adds many new tests, as always.
This commit is contained in:
parent
d3f2bc09b7
commit
686cd93927
17 changed files with 181 additions and 53 deletions
|
@ -1,3 +1,6 @@
|
||||||
import {Type} from '#core/Type.js'
|
import {Type} from '#core/Type.js'
|
||||||
|
|
||||||
export const BooleanT = new Type(n => typeof n === 'boolean')
|
export const BooleanT = new Type(n => typeof n === 'boolean', {
|
||||||
|
zero: false,
|
||||||
|
one: true
|
||||||
|
})
|
||||||
|
|
|
@ -13,6 +13,13 @@ As of this writing, the only two types required to be in a TypeDispatcher are
|
||||||
Undefined (the type inhabited only by `undefined`) and TypeOfTypes (the type
|
Undefined (the type inhabited only by `undefined`) and TypeOfTypes (the type
|
||||||
inhabited exactly by Type objects).
|
inhabited exactly by Type objects).
|
||||||
|
|
||||||
|
There is also a constant NotAType which is the type-world analogue of NaN for
|
||||||
|
numbers. It is occasionally used for the rare behavior that truly does not
|
||||||
|
return any particular type, such as the method `zero` that takes a Type and
|
||||||
|
returns its zero element. However, it does not really work as a Type, and in
|
||||||
|
particular, do _not_ merge it into any TypeDispatcher -- it will disrupt the
|
||||||
|
type and method resolution process.
|
||||||
|
|
||||||
## Core methods
|
## Core methods
|
||||||
|
|
||||||
Similarly, as of this writing the only methods that must be in a TypeDispatcher
|
Similarly, as of this writing the only methods that must be in a TypeDispatcher
|
||||||
|
|
|
@ -2,14 +2,20 @@ export class Type {
|
||||||
constructor(f, options = {}) {
|
constructor(f, options = {}) {
|
||||||
this.test = f
|
this.test = f
|
||||||
this.from = options.from ?? {patterns: []} // mock empty Implementations
|
this.from = options.from ?? {patterns: []} // mock empty Implementations
|
||||||
|
if ('zero' in options) this.zero = options.zero
|
||||||
|
if ('one' in options) this.one = options.one
|
||||||
}
|
}
|
||||||
toString() {
|
toString() {
|
||||||
return this.name || `[Type ${this.test}]`
|
return this.name || `[Type ${this.test}]`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Undefined = new Type(t => typeof t === 'undefined')
|
export const Undefined = new Type(
|
||||||
|
t => typeof t === 'undefined',
|
||||||
|
{zero: undefined, one: undefined})
|
||||||
export const TypeOfTypes = new Type(t => t instanceof Type)
|
export const TypeOfTypes = new Type(t => t instanceof Type)
|
||||||
|
export const NotAType = new Type(t => true) // Danger, do not merge!
|
||||||
|
NotAType._doNotMerge = true
|
||||||
|
|
||||||
export const Returns = (type, f) => (f.returns = type, f)
|
export const Returns = (type, f) => (f.returns = type, f)
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import ArrayKeyedMap from 'array-keyed-map'
|
import ArrayKeyedMap from 'array-keyed-map'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Implementations, ImplementationsGenerator,
|
Implementations, ImplementationsGenerator, ResolutionError,
|
||||||
isPlainFunction, isPlainObject, onType, types
|
isPlainFunction, isPlainObject, onType, types
|
||||||
} from './helpers.js'
|
} from './helpers.js'
|
||||||
import {bootstrapTypes, Returns, whichType, Type} from './Type.js'
|
import {bootstrapTypes, Returns, whichType, Type} from './Type.js'
|
||||||
|
@ -80,6 +80,9 @@ export class TypeDispatcher {
|
||||||
|
|
||||||
// Now dispatch on what sort of thing we are supposed to merge:
|
// Now dispatch on what sort of thing we are supposed to merge:
|
||||||
if (val instanceof Type) {
|
if (val instanceof Type) {
|
||||||
|
if (val._doNotMerge) {
|
||||||
|
throw new TypeError(`attempt to merge unusable type '${val}'`)
|
||||||
|
}
|
||||||
this.types[key] = val
|
this.types[key] = val
|
||||||
val.name = key
|
val.name = key
|
||||||
continue
|
continue
|
||||||
|
@ -219,22 +222,21 @@ export class TypeDispatcher {
|
||||||
// transforms them per the given template, to massage them into the form
|
// transforms them per the given template, to massage them into the form
|
||||||
// expected by a behavior associated with the TypePattern that produced
|
// expected by a behavior associated with the TypePattern that produced
|
||||||
// the template.
|
// the template.
|
||||||
_generateCollectFunction(template, state = {pos: 0}) {
|
_generateCollectFunction(template, state=false) {
|
||||||
const extractors = []
|
if (!Array.isArray(template)) {
|
||||||
for (const elt of template) {
|
const from = state ? state.pos++ : 0
|
||||||
if (Array.isArray(elt)) {
|
let extractor = args => args[from]
|
||||||
extractors.push(this._generateCollectFunction(elt, state))
|
if ('actual' in template) { // incorporate conversion
|
||||||
} else {
|
let convert = template.convertor
|
||||||
const from = state.pos++
|
// Check if it's a factory:
|
||||||
if ('actual' in elt) { // incorporate conversion
|
if (!convert.returns) convert = convert(this, template.actual)
|
||||||
let convert = elt.convertor
|
extractor = args => convert(args[from])
|
||||||
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 state ? extractor : args => [extractor(args)]
|
||||||
}
|
}
|
||||||
|
state ||= {pos: 0}
|
||||||
|
const extractors = template.map(
|
||||||
|
item => this._generateCollectFunction(item, state))
|
||||||
return args => extractors.map(f => f(args))
|
return args => extractors.map(f => f(args))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -284,7 +286,8 @@ export class TypeDispatcher {
|
||||||
template = types
|
template = types
|
||||||
}
|
}
|
||||||
if (needItem) {
|
if (needItem) {
|
||||||
throw new TypeError(`no matching definition of '${key}' on '${types}'`)
|
throw new ResolutionError(
|
||||||
|
`no matching definition of '${key}' on '${types}'`)
|
||||||
}
|
}
|
||||||
// If this key is producing a non-function value, we're done
|
// If this key is producing a non-function value, we're done
|
||||||
if (!isPlainFunction(item)) {
|
if (!isPlainFunction(item)) {
|
||||||
|
@ -316,7 +319,7 @@ export class TypeDispatcher {
|
||||||
matched(template))
|
matched(template))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
e.message = `Error in factory for ${key} on ${types} `
|
e.message = `Error in factory for ${key} on ${types} `
|
||||||
+ `(match data ${template}): ${e}`
|
+ `(match data ${template}): ${e.message}`
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
} else theBehavior = item
|
} else theBehavior = item
|
||||||
|
|
|
@ -3,9 +3,9 @@ import {TypeDispatcher} from '../TypeDispatcher.js'
|
||||||
import * as booleans from '#boolean/all.js'
|
import * as booleans from '#boolean/all.js'
|
||||||
import * as generics from '#generic/all.js'
|
import * as generics from '#generic/all.js'
|
||||||
import * as numbers from '#number/all.js'
|
import * as numbers from '#number/all.js'
|
||||||
import {onType} from "#core/helpers.js"
|
import {onType, ResolutionError} from "#core/helpers.js"
|
||||||
import {Any} from "#core/TypePatterns.js"
|
import {Any} from "#core/TypePatterns.js"
|
||||||
import {Returns} from "#core/Type.js"
|
import {Returns, NotAType} from "#core/Type.js"
|
||||||
import {plain} from "#number/helpers.js"
|
import {plain} from "#number/helpers.js"
|
||||||
|
|
||||||
describe('TypeDispatcher', () => {
|
describe('TypeDispatcher', () => {
|
||||||
|
@ -17,6 +17,7 @@ describe('TypeDispatcher', () => {
|
||||||
const {NumberT, TypeOfTypes, Undefined} = incremental.types
|
const {NumberT, TypeOfTypes, Undefined} = incremental.types
|
||||||
assert(NumberT.test(7))
|
assert(NumberT.test(7))
|
||||||
assert.strictEqual(incremental.add(-1.5, 0.5), -1)
|
assert.strictEqual(incremental.add(-1.5, 0.5), -1)
|
||||||
|
assert.throws(() => incremental.add(7, undefined), ResolutionError)
|
||||||
// Make Undefined act like zero:
|
// Make Undefined act like zero:
|
||||||
incremental.merge({add: onType(
|
incremental.merge({add: onType(
|
||||||
[Undefined, Any], (_m, [_U, T]) => Returns(T, (_a, b) => b),
|
[Undefined, Any], (_m, [_U, T]) => Returns(T, (_a, b) => b),
|
||||||
|
@ -30,7 +31,7 @@ describe('TypeDispatcher', () => {
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
incremental.add.resolve([Undefined, NumberT]).returns,
|
incremental.add.resolve([Undefined, NumberT]).returns,
|
||||||
NumberT)
|
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(NumberT, () => NaN)
|
const alwaysNaN = Returns(NumberT, () => NaN)
|
||||||
incremental.merge({add: onType(
|
incremental.merge({add: onType(
|
||||||
[Undefined, NumberT], alwaysNaN,
|
[Undefined, NumberT], alwaysNaN,
|
||||||
|
@ -63,4 +64,8 @@ describe('TypeDispatcher', () => {
|
||||||
assert(!bgn._behaviors.negate.has([BooleanT]))
|
assert(!bgn._behaviors.negate.has([BooleanT]))
|
||||||
assert.strictEqual(bgn.negate(true), -2)
|
assert.strictEqual(bgn.negate(true), -2)
|
||||||
})
|
})
|
||||||
|
it('disallows merging NotAType', () => {
|
||||||
|
const doomed = new TypeDispatcher()
|
||||||
|
assert.throws(() => doomed.merge({NaT: NotAType}), TypeError)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -36,6 +36,13 @@ export class ImplementationsGenerator {
|
||||||
|
|
||||||
export const types = new ImplementationsGenerator(() => onType(Passthru, {}))
|
export const types = new ImplementationsGenerator(() => onType(Passthru, {}))
|
||||||
|
|
||||||
|
export class ResolutionError extends TypeError {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args)
|
||||||
|
this.name = 'ResolutionError'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const isPlainObject = obj => {
|
export const isPlainObject = obj => {
|
||||||
if (typeof obj !== 'object') return false
|
if (typeof obj !== 'object') return false
|
||||||
if (!obj) return false // excludes null
|
if (!obj) return false // excludes null
|
||||||
|
|
14
src/coretypes/__test__/utils.spec.js
Normal file
14
src/coretypes/__test__/utils.spec.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import assert from 'assert'
|
||||||
|
import math from '#nanomath'
|
||||||
|
|
||||||
|
describe('core type utility functions', () => {
|
||||||
|
it('identifies zero and one elements in most types', () => {
|
||||||
|
assert.strictEqual(math.zero(math.types.NumberT), 0)
|
||||||
|
assert.strictEqual(math.zero(math.types.Undefined), undefined)
|
||||||
|
assert.strictEqual(math.one(math.types.BooleanT), true)
|
||||||
|
assert.throws(() => math.one(math.types.TypeOfTypes), RangeError)
|
||||||
|
assert.strictEqual(math.one(-7.5), 1)
|
||||||
|
assert.strictEqual(math.one(undefined), undefined)
|
||||||
|
assert.strictEqual(math.zero(true), false)
|
||||||
|
})
|
||||||
|
})
|
|
@ -1 +1,2 @@
|
||||||
export * from './relational.js'
|
export * from './relational.js'
|
||||||
|
export * from './utils.js'
|
||||||
|
|
26
src/coretypes/utils.js
Normal file
26
src/coretypes/utils.js
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import {onType} from '#core/helpers.js'
|
||||||
|
import {NotAType, Returns, TypeOfTypes} from '#core/Type.js'
|
||||||
|
import {Any} from "#core/TypePatterns.js"
|
||||||
|
|
||||||
|
export const zero = onType(
|
||||||
|
Any, (math, T) => {
|
||||||
|
const z = math.zero(T)
|
||||||
|
return Returns(T, () => z)
|
||||||
|
},
|
||||||
|
TypeOfTypes, Returns(NotAType, t => {
|
||||||
|
if ('zero' in t) return t.zero
|
||||||
|
throw new RangeError(`type '${t}' has no zero element`)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
export const one = onType(
|
||||||
|
Any, (math, T) => {
|
||||||
|
const unit = math.one(T)
|
||||||
|
return Returns(T, () => unit)
|
||||||
|
},
|
||||||
|
TypeOfTypes, Returns(NotAType, t => {
|
||||||
|
if ('one' in t) return t.one
|
||||||
|
throw new RangeError(
|
||||||
|
`type '${t}' has no unit element designated as "one"`)
|
||||||
|
})
|
||||||
|
)
|
|
@ -2,6 +2,7 @@ import assert from 'assert'
|
||||||
import math from '#nanomath'
|
import math from '#nanomath'
|
||||||
import * as numbers from '#number/all.js'
|
import * as numbers from '#number/all.js'
|
||||||
import * as generics from '#generic/all.js'
|
import * as generics from '#generic/all.js'
|
||||||
|
import {ResolutionError} from '#core/helpers.js'
|
||||||
import {TypeDispatcher} from '#core/TypeDispatcher.js'
|
import {TypeDispatcher} from '#core/TypeDispatcher.js'
|
||||||
|
|
||||||
describe('generic relational functions', () => {
|
describe('generic relational functions', () => {
|
||||||
|
@ -38,4 +39,15 @@ describe('generic relational functions', () => {
|
||||||
assert.strictEqual(equal(0, 1.1e-15), 1)
|
assert.strictEqual(equal(0, 1.1e-15), 1)
|
||||||
assert.strictEqual(equal(0, 1.1e-13), 0)
|
assert.strictEqual(equal(0, 1.1e-13), 0)
|
||||||
})
|
})
|
||||||
|
it('performs three-way comparison', () => {
|
||||||
|
const {compare} = math
|
||||||
|
assert.strictEqual(compare(-0.4e-15, +0.4e-15), 0)
|
||||||
|
assert.strictEqual(compare(2.2, true), 1)
|
||||||
|
assert.strictEqual(compare(-Infinity, 7), -1)
|
||||||
|
assert(isNaN(compare(NaN, 0)))
|
||||||
|
assert(isNaN(compare(false, NaN)))
|
||||||
|
assert(isNaN(compare(NaN, NaN)))
|
||||||
|
assert.strictEqual(compare(true, false), 1)
|
||||||
|
assert.throws(() => compare(undefined, -1), ResolutionError)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
13
src/generic/__test__/utils.spec.js
Normal file
13
src/generic/__test__/utils.spec.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import assert from 'assert'
|
||||||
|
import math from '#nanomath'
|
||||||
|
|
||||||
|
describe('generic utility functions', () => {
|
||||||
|
it('tests whether an element is zero', () => {
|
||||||
|
const {isZero} = math
|
||||||
|
assert(!isZero(3))
|
||||||
|
assert(isZero(3e-16))
|
||||||
|
assert(isZero(false))
|
||||||
|
assert(!isZero(true))
|
||||||
|
assert(isZero(undefined))
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,3 +1,4 @@
|
||||||
export * as arithmetic from './arithmetic.js'
|
export * as arithmetic from './arithmetic.js'
|
||||||
export * as configuration from './config.js'
|
export * as configuration from './config.js'
|
||||||
export * as relational from './relational.js'
|
export * as relational from './relational.js'
|
||||||
|
export * as utilities from './utils.js'
|
||||||
|
|
|
@ -1,36 +1,52 @@
|
||||||
import {ReturnsAs} from './helpers.js'
|
import {ReturnsAs} from './helpers.js'
|
||||||
import {onType} from '#core/helpers.js'
|
import {onType} from '#core/helpers.js'
|
||||||
|
import {Returns} from '#core/Type.js'
|
||||||
import {Any, matched} from '#core/TypePatterns.js'
|
import {Any, matched} from '#core/TypePatterns.js'
|
||||||
import {boolnum} from '#number/helpers.js'
|
import {boolnum} from '#number/helpers.js'
|
||||||
|
import {NumberT} from '#number/NumberT.js'
|
||||||
|
|
||||||
export const equal = onType(
|
export const equal = onType([Any, Any], (math, [T, U]) => {
|
||||||
[Any, Any], (math, [T, U]) => {
|
// Finding the correct signature of `indistinguishable` to use for
|
||||||
// Finding the correct signature of `indistinguishable` to use for
|
// testing (approximate) equality is tricky, because T or U might
|
||||||
// testing (approximate) equality is tricky, because T or U might
|
// need to be converted for the sake of comparison, and some types
|
||||||
// need to be converted for the sake of comparison, and some types
|
// allow tolerances for equality and others don't. So the plan is
|
||||||
// allow tolerances for equality and others don't. So the plan is
|
// we first look up without tolerances, then we check the config for
|
||||||
// we first look up without tolerances, then we check the config for
|
// the matching type, and then we look up with tolerances.
|
||||||
// the matching type, and then we look up with tolerances.
|
let exactChecker
|
||||||
let exactChecker
|
try {
|
||||||
|
exactChecker = math.indistinguishable.resolve([T, U])
|
||||||
|
} catch { // can't compare, so no way they can be equal
|
||||||
|
return boolnum(() => false)(math)
|
||||||
|
}
|
||||||
|
// Get the type of the first argument to the matching checker:
|
||||||
|
const ByType = matched(exactChecker.template).flat()[0]
|
||||||
|
// Now see if there are tolerances for that type:
|
||||||
|
const typeConfig = math.resolve('config', [ByType])
|
||||||
|
if ('relTol' in typeConfig) {
|
||||||
try {
|
try {
|
||||||
exactChecker = math.indistinguishable.resolve([T, U])
|
const {relTol, absTol} = typeConfig
|
||||||
} catch { // can't compare, so no way they can be equal
|
const RT = math.typeOf(relTol)
|
||||||
return boolnum(() => false)(math)
|
const AT = math.typeOf(absTol)
|
||||||
}
|
const approx = math.indistinguishable.resolve([T, U, RT, AT])
|
||||||
// Get the type of the first argument to the matching checker:
|
return ReturnsAs(
|
||||||
const ByType = matched(exactChecker.template).flat()[0]
|
approx, (t, u) => approx(t, u, relTol, absTol))
|
||||||
// Now see if there are tolerances for that type:
|
} catch {} // fall through to case with no tolerances
|
||||||
const typeConfig = math.resolve('config', [ByType])
|
}
|
||||||
if ('relTol' in typeConfig) {
|
// either no tolerances or no matching signature for indistinguishable
|
||||||
try {
|
return exactChecker
|
||||||
const {relTol, absTol} = typeConfig
|
})
|
||||||
const RT = math.typeOf(relTol)
|
|
||||||
const AT = math.typeOf(absTol)
|
// now that we have `equal` and `exceeds`, pretty much everything else should
|
||||||
const approx = math.indistinguishable.resolve([T, U, RT, AT])
|
// be easy:
|
||||||
return ReturnsAs(
|
|
||||||
approx, (t, u) => approx(t, u, relTol, absTol))
|
export const compare = onType([Any, Any], (math, [T, U]) => {
|
||||||
} catch {} // fall through to case with no tolerances
|
const eq = math.equal.resolve([T, U])
|
||||||
}
|
const gt = math.exceeds.resolve([T, U])
|
||||||
// either no tolerances or no matching signature for indistinguishable
|
const isTnan = math.isnan.resolve([T])
|
||||||
return exactChecker
|
const isUnan = math.isnan.resolve([U])
|
||||||
})
|
return Returns(NumberT, (t, u) => {
|
||||||
|
if (isTnan(t) || isUnan(u)) return NaN
|
||||||
|
if (eq(t,u)) return 0
|
||||||
|
return gt(t, u) ? 1 : -1
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
10
src/generic/utils.js
Normal file
10
src/generic/utils.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import {ReturnsAs} from './helpers.js'
|
||||||
|
import {onType} from '#core/helpers.js'
|
||||||
|
import {Returns} from '#core/Type.js'
|
||||||
|
import {Any} from "#core/TypePatterns.js"
|
||||||
|
|
||||||
|
export const isZero = (math, [T]) => {
|
||||||
|
const z = math.zero(T)
|
||||||
|
const eq = math.equal.resolve([T, T])
|
||||||
|
return ReturnsAs(eq, x => eq(z, x))
|
||||||
|
}
|
|
@ -4,4 +4,6 @@ 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: onType(BooleanT, math => math.number.resolve([BooleanT])),
|
from: onType(BooleanT, math => math.number.resolve([BooleanT])),
|
||||||
|
one: 1,
|
||||||
|
zero: 0
|
||||||
})
|
})
|
||||||
|
|
|
@ -12,6 +12,7 @@ describe('number relational functions', () => {
|
||||||
assert(exceeds(-1e101, -Infinity))
|
assert(exceeds(-1e101, -Infinity))
|
||||||
assert(!exceeds(NaN, 0))
|
assert(!exceeds(NaN, 0))
|
||||||
assert(!exceeds(0, NaN))
|
assert(!exceeds(0, NaN))
|
||||||
|
assert(!exceeds(NaN, NaN))
|
||||||
assert(!exceeds(2, 2))
|
assert(!exceeds(2, 2))
|
||||||
})
|
})
|
||||||
it('checks for exact equality', () => {
|
it('checks for exact equality', () => {
|
||||||
|
|
|
@ -7,6 +7,7 @@ const num = f => Returns(NumberT, f)
|
||||||
|
|
||||||
export const number = plain(a => a)
|
export const number = plain(a => a)
|
||||||
number.also(
|
number.also(
|
||||||
BooleanT, num(p => p ? 1 : 0),
|
// conversions from Boolean should be consistent with one and zero:
|
||||||
|
BooleanT, num(p => p ? NumberT.one : NumberT.zero),
|
||||||
[], num(() => 0)
|
[], num(() => 0)
|
||||||
)
|
)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue