Introduce BooleanT and boolean functions #17

Merged
glen merged 7 commits from booleans into main 2025-04-13 16:29:53 +00:00
11 changed files with 151 additions and 61 deletions
Showing only changes of commit f38a2d5e88 - Show all commits

View file

@ -18,4 +18,7 @@
devDependencies: { devDependencies: {
mocha: '^11.1.0', mocha: '^11.1.0',
}, },
dependencies: {
'array-keyed-map': '^2.1.3',
},
} }

9
pnpm-lock.yaml generated
View file

@ -7,6 +7,10 @@ settings:
importers: importers:
.: .:
dependencies:
array-keyed-map:
specifier: ^2.1.3
version: 2.1.3
devDependencies: devDependencies:
mocha: mocha:
specifier: ^11.1.0 specifier: ^11.1.0
@ -49,6 +53,9 @@ packages:
argparse@2.0.1: argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} 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: balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
@ -398,6 +405,8 @@ snapshots:
argparse@2.0.1: {} argparse@2.0.1: {}
array-keyed-map@2.1.3: {}
balanced-match@1.0.2: {} balanced-match@1.0.2: {}
binary-extensions@2.3.0: {} binary-extensions@2.3.0: {}

View file

@ -1,3 +1,5 @@
import {onType} from './helpers.js'
const typeObject = {} // have to make sure there is only one const typeObject = {} // have to make sure there is only one
export const types = () => typeObject export const types = () => typeObject
@ -5,7 +7,8 @@ export const types = () => typeObject
export class Type { export class Type {
constructor(f, options = {}) { constructor(f, options = {}) {
this.test = f this.test = f
this.from = new Map(options.from ?? []) this.from = options.from ?? onType() // empty Implementations if no ...
// ... conversions specified
} }
toString() { toString() {
return this.name || `[Type ${this.test}]` return this.name || `[Type ${this.test}]`

View file

@ -1,11 +1,10 @@
import {typeOf, Type, bootstrapTypes} from './Type.js' import ArrayKeyedMap from 'array-keyed-map'
import { import {
Implementations, isPlainFunction, isPlainObject, onType Implementations, isPlainFunction, isPlainObject, onType
} from './helpers.js' } from './helpers.js'
import { import {bootstrapTypes, Returns, typeOf, Type} from './Type.js'
alwaysMatches, matched, needsCollection, Any, Multiple import {matched, needsCollection, Passthru} from './TypePatterns.js'
} from './TypePatterns.js'
export class TypeDispatcher { export class TypeDispatcher {
constructor(...specs) { constructor(...specs) {
@ -37,25 +36,39 @@ export class TypeDispatcher {
continue continue
} }
if (typeof val === 'function') { if (typeof val === 'function') {
val = onType(Multiple(Any), val) val = onType(Passthru, val)
} }
if (val instanceof Implementations) { if (val instanceof Implementations) {
if (!(key in this)) { 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, { Object.defineProperty(this, key, {
enumerable: true, enumerable: true,
configurable: true, configurable: true,
get: () => { get: () => {
let tryValue let tryValue
let tryTypes = []
try { try {
tryValue = this.resolve(key, []) tryValue = this.resolve(key, [])
} catch { } catch {
// Has no value for the empty type list, so // Has no value for the empty type list, so therefore
// find a type list it will have a value for // it must be a method, as there is no way to supply
tryTypes = // any types for a non-function value. Hence, we can
this._implementations[key][0][0].sampleTypes() // just make tryValue any plain function, since it is
tryValue = this.resolve(key, tryTypes) // never actually used, just its type analyzed.
tryValue = () => undefined
} }
// Redefine the property according to what sort of // Redefine the property according to what sort of
// entity it is: // entity it is:
@ -74,7 +87,7 @@ export class TypeDispatcher {
}) })
return standard return standard
} }
if (tryTypes.length) tryValue = undefined
if (typeof tryValue === 'object') { if (typeof tryValue === 'object') {
if (!('resolve' in tryValue)) { if (!('resolve' in tryValue)) {
tryValue.resolve = types => this.resolve(key, types) tryValue.resolve = types => this.resolve(key, types)
@ -92,6 +105,7 @@ export class TypeDispatcher {
}) })
return get() return get()
} }
Object.defineProperty(this, key, { Object.defineProperty(this, key, {
enumerable: true, enumerable: true,
configurable: true, configurable: true,
@ -100,13 +114,16 @@ export class TypeDispatcher {
return tryValue return tryValue
} }
}) })
// Finally, initialize the other data for this key:
this._implementations[key] = [] this._implementations[key] = []
this._behaviors[key] = new Map() this._behaviors[key] = new ArrayKeyedMap()
this._dependencies[key] = new Map() this._dependencies[key] = new ArrayKeyedMap()
} }
// Now add all of the patterns of this implementation: // Now add all of the patterns of this implementation:
for (const [pattern, result] of val.patterns) { for (const [pattern, result] of val.patterns) {
if (alwaysMatches(pattern)) { if (pattern === Passthru) {
if (key in this._fallbacks) this._disengageFallback(key) if (key in this._fallbacks) this._disengageFallback(key)
this._fallbacks[key] = result this._fallbacks[key] = result
} else { } else {
@ -128,7 +145,7 @@ export class TypeDispatcher {
} }
// install value as a catchall value // install value as a catchall value
this.merge({[key]: onType(Multiple(Any), val)}) this.merge({[key]: onType(Passthru, val)})
} }
} }
@ -151,7 +168,10 @@ export class TypeDispatcher {
} else { } else {
const from = state.pos++ const from = state.pos++
if ('actual' in elt) { // incorporate conversion if ('actual' in elt) { // incorporate conversion
const convert = elt.matched.from.get(elt.actual)(this) 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])) extractors.push(args => convert(args[from]))
} else extractors.push(args => args[from]) } else extractors.push(args => args[from])
} }
@ -167,14 +187,17 @@ export class TypeDispatcher {
if (this.resolve._genDepsOf?.length) this._addToDeps(key, types) if (this.resolve._genDepsOf?.length) this._addToDeps(key, types)
const behave = this._behaviors[key] const behave = this._behaviors[key]
// Return the cached resolution if it's there
if (behave.has(types)) return behave.get(types) if (behave.has(types)) return behave.get(types)
// Otherwise, perform the resolution and cache the result
const imps = this._implementations[key] const imps = this._implementations[key]
let needItem = true let needItem = true
let item let item
let pattern
let template let template
if (imps.length) { if (imps.length) {
for (const options of [{}, {convert: true}]) { for (const options of [{}, {convert: true}]) {
let pattern
for ([pattern, item] of imps) { for ([pattern, item] of imps) {
let finalIndex let finalIndex
;[finalIndex, template] = pattern.match(types, options) ;[finalIndex, template] = pattern.match(types, options)
@ -189,50 +212,59 @@ export class TypeDispatcher {
if (needItem && key in this._fallbacks) { if (needItem && key in this._fallbacks) {
needItem = false needItem = false
item = this._fallbacks[key] item = this._fallbacks[key]
template = [types] template = types
} }
if (needItem) { if (needItem) {
throw new TypeError(`no matching definition of '${key}' on '${types}'`) 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) behave.set(types, item)
return 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 // First set up to record dependencies
if (!('_genDepsOf' in this.resolve)) { if (!('_genDepsOf' in this.resolve)) {
this.resolve._genDepsOf = [] this.resolve._genDepsOf = []
} }
this.resolve._genDepsOf.push([key, types])
let theBehavior = () => undefined let theBehavior = () => undefined
try { this.resolve._genDepsOf.push([key, types]) // Important: make sure
theBehavior = item( // not to return without popping _genDepsOf
DependencyRecorder(this, [], this), matched(template)) if (!('returns' in item)) {
} catch { // looks like a factory
// 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 {
this.resolve._genDepsOf.pop()
}
if (typeof theBehavior === 'function'
&& theBehavior.length
&& needsCollection(template)
) {
// have to wrap the behavior to collect the actual arguments
// in the way corresponding to the template. That may generate
// more dependencies:
this.resolve._genDepsOf.push([key, types])
try { try {
const collectFunction = this._generateCollectFunction(template) theBehavior = item(
theBehavior = (...args) => theBehavior(...collectFunction(args)) DependencyRecorder(this, [], this), matched(template))
} finally { } catch {
this.resolve._genDepsOf.pop() // 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)))
} }
} }
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 // Method called to invalidate a set of behaviors

View file

@ -20,8 +20,13 @@ class MatchTypePattern extends TypePattern {
const actual = typeSequence[position] const actual = typeSequence[position]
if (position < typeSequence.length) { if (position < typeSequence.length) {
if (actual === this.type) return [position + 1, actual] if (actual === this.type) return [position + 1, actual]
if (options.convert && this.type.from.has(actual)) { if (options.convert) {
return [position + 1, {actual, matched: this.type}] 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] return [-1, Undefined]
@ -131,8 +136,18 @@ class MultiPattern extends TypePattern {
} }
export const Multiple = item => new MultiPattern(item) 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 // returns the template just of matched types, dropping any actual types
export const matched = (template) => { export const matched = (template) => {

View file

@ -1,7 +1,8 @@
import assert from 'assert' import assert from 'assert'
import {TypeDispatcher} from '../TypeDispatcher.js' import {TypeDispatcher} from '../TypeDispatcher.js'
import * as numbers from '#number/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 {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"
@ -47,4 +48,15 @@ describe('TypeDispatcher', () => {
gnmath.merge({multiply: plain((a,b) => Math.floor(a) * b)}) gnmath.merge({multiply: plain((a,b) => Math.floor(a) * b)})
assert.strictEqual(gnmath.square(-2.5), 7.5) 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)
})
}) })

View file

@ -1,5 +1,7 @@
import assert from 'assert' 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' import {Undefined, TypeOfTypes} from '../Type.js'
describe('Type patterns', () => { describe('Type patterns', () => {
@ -93,4 +95,10 @@ describe('Type patterns', () => {
whyNot.sampleTypes(), [Undefined, TypeOfTypes]) whyNot.sampleTypes(), [Undefined, TypeOfTypes])
assert(whyNot.equal(whyNot)) 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}]))
})
}) })

View file

@ -9,8 +9,8 @@ describe('Core helpers', () => {
it('defines what Implementations are', () => { it('defines what Implementations are', () => {
const imps = onType(Undefined, 7, [TypeOfTypes, Undefined], -3) const imps = onType(Undefined, 7, [TypeOfTypes, Undefined], -3)
assert(imps instanceof Implementations) assert(imps instanceof Implementations)
assert(imps.patterns instanceof Map) assert(imps.patterns instanceof Array)
assert(imps.patterns.keys().every(k => k instanceof TypePattern)) assert(imps.patterns.every(([k]) => k instanceof TypePattern))
}) })
it('detects plain objects', () => { it('detects plain objects', () => {

View file

@ -2,12 +2,12 @@ import {pattern} from './TypePatterns.js'
export class Implementations { export class Implementations {
constructor(imps) { constructor(imps) {
this.patterns = new Map() this.patterns = []
this._add(imps) this._add(imps)
} }
_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.push([pattern(imps[i]), imps[++i]])
} }
} }
also(...imps) { also(...imps) {

View file

@ -1,6 +1,7 @@
import {Type} from '#core/Type.js' import {Type} from '#core/Type.js'
import {onType} from '#core/helpers.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, math => math.number.resolve([BooleanT])]], from: onType(BooleanT, math => math.number.resolve([BooleanT])),
}) })

View file

@ -12,7 +12,14 @@ describe('NumberT Type', () => {
}) })
it('can convert from BooleanT to NumberT', () => { it('can convert from BooleanT to NumberT', () => {
const cnvBtoN = NumberT.from.get(BooleanT)(math) 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(true), 1)
assert.strictEqual(cnvBtoN(false), 0) assert.strictEqual(cnvBtoN(false), 0)
}) })