feat(Types): Improve type spec and ignore signatures that use unknown type #23

Merged
glen merged 1 commits from forgiving_types into main 2022-07-24 19:53:41 +00:00
7 changed files with 129 additions and 70 deletions

View File

@ -1,3 +1,6 @@
export const Types = { export const Types = {
bigint: {test: b => typeof b === 'bigint'} bigint: {
before: ['Complex'],
test: b => typeof b === 'bigint'
}
} }

View File

@ -9,10 +9,12 @@ export function isComplex(z) {
export const Types = { export const Types = {
Complex: { Complex: {
test: isComplex, test: isComplex,
from: {
number: x => ({re: x, im: 0}), number: x => ({re: x, im: 0}),
bigint: x => ({re: x, im: 0n}) bigint: x => ({re: x, im: 0n})
} }
} }
}
/* test if an entity is Complex<number>, so to speak: */ /* test if an entity is Complex<number>, so to speak: */
export function numComplex(z) { export function numComplex(z) {

View File

@ -1,6 +1,7 @@
/* Core of pocomath: create an instance */ /* Core of pocomath: create an instance */
import typed from 'typed-function' import typed from 'typed-function'
import dependencyExtractor from './dependencyExtractor.mjs' import dependencyExtractor from './dependencyExtractor.mjs'
import {subsetOfKeys, typesOfSignature} from './utils.mjs'
export default class PocomathInstance { export default class PocomathInstance {
/* Disallowed names for ops; beware, this is slightly non-DRY /* Disallowed names for ops; beware, this is slightly non-DRY
@ -15,8 +16,7 @@ export default class PocomathInstance {
this._affects = {} this._affects = {}
this._typed = typed.create() this._typed = typed.create()
this._typed.clear() this._typed.clear()
// Convenient hack for now, would remove when a real string type is added: this.Types = {any: {}} // dummy entry to track the default 'any' type
this._typed.addTypes([{name: 'string', test: s => typeof s === 'string'}])
} }
/** /**
@ -54,13 +54,23 @@ export default class PocomathInstance {
* *
* Note that the "operation" named `Types` is special: it gives * Note that the "operation" named `Types` is special: it gives
* types that must be installed in the instance. In this case, the keys * types that must be installed in the instance. In this case, the keys
* are type names, and the values are objects with a property 'test' * are type names, and the values are plain objects with the following
* giving the predicate for the type, and properties for each type that can * properties:
* be converted **to** this type, giving the corresponding conversion *
* function. * - test: the predicate for the type
* - from: a plain object mapping the names of types that can be converted
* **to** this type to the corresponding conversion functions
* - before: [optional] a list of types this should be added
* before, in priority order
*/ */
install(ops) { install(ops) {
for (const key in ops) this._installOp(key, ops[key]) for (const [item, spec] of Object.entries(ops)) {
if (item === 'Types') {
this._installTypes(spec)
} else {
this._installOp(item, spec)
}
}
} }
/** /**
@ -100,6 +110,45 @@ export default class PocomathInstance {
} }
} }
/* Used internally by install, see the documentation there.
* Note that unlike _installOp below, we can do this immediately
*/
_installTypes(typeSpecs) {
for (const [type, spec] of Object.entries(typeSpecs)) {
if (type in this.Types) {
if (spec !== this.Types[type]) {
throw new SyntaxError(
`Conflicting definitions of type ${type}`)
}
continue
}
let beforeType = 'any'
for (const other of spec.before || []) {
if (other in this.Types) {
beforeType = other
break
}
}
this._typed.addTypes([{name: type, test: spec.test}], beforeType)
/* Now add conversions to this type */
for (const from in (spec.from || {})) {
if (from in this.Types) {
this._typed.addConversion(
{from, to: type, convert: spec.from[from]})
}
}
/* And add conversions from this type */
for (const to in this.Types) {
if (type in (this.Types[to].from || {})) {
this._typed.addConversion(
{from: type, to, convert: this.Types[to].from[type]})
}
}
this.Types[type] = spec
this._invalidate(':' + type) // rebundle anything that uses the new type
}
}
/* Used internally by install, see the documentation there */ /* Used internally by install, see the documentation there */
_installOp(name, implementations) { _installOp(name, implementations) {
if (name.charAt(0) === '_') { if (name.charAt(0) === '_') {
@ -115,42 +164,35 @@ export default class PocomathInstance {
this._invalidate(name) this._invalidate(name)
const opImps = this._imps[name] const opImps = this._imps[name]
for (const [signature, does] of Object.entries(implementations)) { for (const [signature, does] of Object.entries(implementations)) {
if (name === 'Types') {
if (signature in opImps) {
if (does != opImps[signature]) {
throw newSyntaxError(
`Conflicting definitions of type ${signature}`)
}
} else {
opImps[signature] = does
}
continue
}
if (signature in opImps) { if (signature in opImps) {
if (does !== opImps[signature].does) { if (does !== opImps[signature].does) {
throw new SyntaxError( throw new SyntaxError(
`Conflicting definitions of ${signature} for ${name}`) `Conflicting definitions of ${signature} for ${name}`)
} }
} else { } else {
if (name === 'Types') {
opImps[signature] = does
continue
}
const uses = new Set() const uses = new Set()
does(dependencyExtractor(uses)) does(dependencyExtractor(uses))
opImps[signature] = {uses, does} opImps[signature] = {uses, does}
for (const dep of uses) { for (const dep of uses) {
const depname = dep.split('(', 1)[0] const depname = dep.split('(', 1)[0]
if (depname === 'self') continue if (depname === 'self') continue
if (!(depname in this._affects)) { this._addAffect(depname, name)
this._affects[depname] = new Set()
} }
this._affects[depname].add(name) for (const type of typesOfSignature(signature)) {
this._addAffect(':' + type, name)
} }
} }
} }
} }
_addAffect(dependency, dependent) {
if (dependency in this._affects) {
this._affects[dependency].add(dependent)
} else {
this._affects[dependency] = new Set([dependent])
}
}
/** /**
* Reset an operation to require creation of typed-function, * Reset an operation to require creation of typed-function,
* and if it has no implementations so far, set them up. * and if it has no implementations so far, set them up.
@ -177,12 +219,18 @@ export default class PocomathInstance {
*/ */
_bundle(name) { _bundle(name) {
const imps = this._imps[name] const imps = this._imps[name]
if (!imps || Object.keys(imps).length === 0) { if (!imps) {
throw new SyntaxError(`No implementations for ${name}`) throw new SyntaxError(`No implementations for ${name}`)
} }
this._ensureTypes() const usableEntries = Object.entries(imps).filter(
([signature]) => subsetOfKeys(typesOfSignature(signature), this.Types))
if (usableEntries.length === 0) {
throw new SyntaxError(
`Every implementation for ${name} uses an undefined type;\n`
+ ` signatures: ${Object.keys(imps)}`)
}
const tf_imps = {} const tf_imps = {}
for (const [signature, {uses, does}] of Object.entries(imps)) { for (const [signature, {uses, does}] of usableEntries) {
if (uses.length === 0) { if (uses.length === 0) {
tf_imps[signature] = does() tf_imps[signature] = does()
} else { } else {
@ -214,36 +262,4 @@ export default class PocomathInstance {
return tf return tf
} }
/**
* Ensure that all of the requested types and conversions are actually
* in the typed-function universe:
*/
_ensureTypes() {
const newTypes = []
const newTypeSet = new Set()
const knownTypeSet = new Set()
const conversions = []
const typeSpec = this._imps.Types
for (const name in this._imps.Types) {
knownTypeSet.add(name)
for (const from in typeSpec[name]) {
if (from === 'test') continue;
conversions.push(
{from, to: name, convert: typeSpec[name][from]})
}
try { // Hack: work around typed-function #154
this._typed._findType(name)
} catch {
newTypeSet.add(name)
newTypes.push({name, test: typeSpec[name].test})
}
}
this._typed.addTypes(newTypes)
const newConversions = conversions.filter(
item => (newTypeSet.has(item.from) || newTypeSet.has(item.to)) &&
knownTypeSet.has(item.from) && knownTypeSet.has(item.to)
)
this._typed.addConversions(newConversions)
}
} }

12
src/core/utils.mjs Normal file
View File

@ -0,0 +1,12 @@
/* Returns true if set is a subset of the keys of obj */
export function subsetOfKeys(set, obj) {
for (const e of set) {
if (!(e in obj)) return false
}
return true
}
/* Returns a set of all of the types mentioned in a typed-function signature */
export function typesOfSignature(signature) {
return new Set(signature.split(/[^\w\d]/).filter(s => s.length))
}

View File

@ -1,7 +1,8 @@
export const Types = { export const Types = {
number: { number: {
before: ['Complex'],
test: n => typeof n === 'number', test: n => typeof n === 'number',
string: s => +s from: {string: s => +s}
} }
} }

View File

@ -18,10 +18,18 @@ describe('The default full pocomath instance "math"', () => {
}) })
it('can be extended', () => { it('can be extended', () => {
math.install({'add': { math.install({
'...string': () => addends => addends.reduce((x,y) => x+y, '') add: {
}}) '...stringK': () => addends => addends.reduce((x,y) => x+y, '')
assert.strictEqual(math.add('Kilroy',' is here'), 'Kilroy is here') },
Types: {
stringK: {
test: s => typeof s === 'string' && s.charAt(0) === 'K',
before: ['string']
}
}
})
assert.strictEqual(math.add('Kilroy','K is here'), 'KilroyK is here')
}) })
it('handles complex numbers', () => { it('handles complex numbers', () => {

View File

@ -44,6 +44,23 @@ describe('A custom instance', () => {
pm.subtract({re:5, im:0}, {re:10, im:1}), {re:-5, im: -1}) pm.subtract({re:5, im:0}, {re:10, im:1}), {re:-5, im: -1})
}) })
it("can defer definition of (even used) types", () => {
const dt = new PocomathInstance('Deferred Types')
dt.install(numberAdd)
dt.install({times: {
'number,number': () => (m,n) => m*n,
'Complex,Complex': ({complex}) => (w,z) => {
return complex(w.re*z.re - w.im*z.im, w.re*z.im + w.im*z.re)
}
}})
// complex type not present but should still be able to add numbers:
assert.strictEqual(dt.times(3,5), 15)
dt.install(complexComplex)
// times should now rebundle to allow complex:
assert.deepStrictEqual(
dt.times(dt.complex(2,3), dt.complex(2,-3)), dt.complex(13))
})
it("can selectively import in cute ways", async function () { it("can selectively import in cute ways", async function () {
const cherry = new PocomathInstance('cherry') const cherry = new PocomathInstance('cherry')
cherry.install(numberAdd) cherry.install(numberAdd)