Merge pull request 'feat(Types): Improve type spec and ignore signatures that use unknown type' (#23) from forgiving_types into main
Reviewed-on: #23
This commit is contained in:
commit
79f261ff65
@ -1,3 +1,6 @@
|
||||
export const Types = {
|
||||
bigint: {test: b => typeof b === 'bigint'}
|
||||
bigint: {
|
||||
before: ['Complex'],
|
||||
test: b => typeof b === 'bigint'
|
||||
}
|
||||
}
|
||||
|
@ -9,8 +9,10 @@ export function isComplex(z) {
|
||||
export const Types = {
|
||||
Complex: {
|
||||
test: isComplex,
|
||||
number: x => ({re: x, im: 0}),
|
||||
bigint: x => ({re: x, im: 0n})
|
||||
from: {
|
||||
number: x => ({re: x, im: 0}),
|
||||
bigint: x => ({re: x, im: 0n})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
/* Core of pocomath: create an instance */
|
||||
import typed from 'typed-function'
|
||||
import dependencyExtractor from './dependencyExtractor.mjs'
|
||||
import {subsetOfKeys, typesOfSignature} from './utils.mjs'
|
||||
|
||||
export default class PocomathInstance {
|
||||
/* Disallowed names for ops; beware, this is slightly non-DRY
|
||||
@ -15,10 +16,9 @@ export default class PocomathInstance {
|
||||
this._affects = {}
|
||||
this._typed = typed.create()
|
||||
this._typed.clear()
|
||||
// Convenient hack for now, would remove when a real string type is added:
|
||||
this._typed.addTypes([{name: 'string', test: s => typeof s === 'string'}])
|
||||
this.Types = {any: {}} // dummy entry to track the default 'any' type
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* (Partially) define one or more operations of the instance:
|
||||
*
|
||||
@ -54,13 +54,23 @@ export default class PocomathInstance {
|
||||
*
|
||||
* Note that the "operation" named `Types` is special: it gives
|
||||
* 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'
|
||||
* giving the predicate for the type, and properties for each type that can
|
||||
* be converted **to** this type, giving the corresponding conversion
|
||||
* function.
|
||||
* are type names, and the values are plain objects with the following
|
||||
* properties:
|
||||
*
|
||||
* - 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) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -99,6 +109,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 */
|
||||
_installOp(name, implementations) {
|
||||
@ -115,42 +164,35 @@ export default class PocomathInstance {
|
||||
this._invalidate(name)
|
||||
const opImps = this._imps[name]
|
||||
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 (does !== opImps[signature].does) {
|
||||
throw new SyntaxError(
|
||||
`Conflicting definitions of ${signature} for ${name}`)
|
||||
}
|
||||
} else {
|
||||
if (name === 'Types') {
|
||||
opImps[signature] = does
|
||||
continue
|
||||
}
|
||||
const uses = new Set()
|
||||
does(dependencyExtractor(uses))
|
||||
opImps[signature] = {uses, does}
|
||||
for (const dep of uses) {
|
||||
const depname = dep.split('(', 1)[0]
|
||||
if (depname === 'self') continue
|
||||
if (!(depname in this._affects)) {
|
||||
this._affects[depname] = new Set()
|
||||
}
|
||||
this._affects[depname].add(name)
|
||||
this._addAffect(depname, 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,
|
||||
* and if it has no implementations so far, set them up.
|
||||
@ -177,12 +219,18 @@ export default class PocomathInstance {
|
||||
*/
|
||||
_bundle(name) {
|
||||
const imps = this._imps[name]
|
||||
if (!imps || Object.keys(imps).length === 0) {
|
||||
if (!imps) {
|
||||
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 = {}
|
||||
for (const [signature, {uses, does}] of Object.entries(imps)) {
|
||||
for (const [signature, {uses, does}] of usableEntries) {
|
||||
if (uses.length === 0) {
|
||||
tf_imps[signature] = does()
|
||||
} else {
|
||||
@ -214,36 +262,4 @@ export default class PocomathInstance {
|
||||
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
12
src/core/utils.mjs
Normal 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))
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
export const Types = {
|
||||
number: {
|
||||
before: ['Complex'],
|
||||
test: n => typeof n === 'number',
|
||||
string: s => +s
|
||||
from: {string: s => +s}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -18,10 +18,18 @@ describe('The default full pocomath instance "math"', () => {
|
||||
})
|
||||
|
||||
it('can be extended', () => {
|
||||
math.install({'add': {
|
||||
'...string': () => addends => addends.reduce((x,y) => x+y, '')
|
||||
}})
|
||||
assert.strictEqual(math.add('Kilroy',' is here'), 'Kilroy is here')
|
||||
math.install({
|
||||
add: {
|
||||
'...stringK': () => addends => addends.reduce((x,y) => x+y, '')
|
||||
},
|
||||
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', () => {
|
||||
|
@ -44,6 +44,23 @@ describe('A custom instance', () => {
|
||||
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 () {
|
||||
const cherry = new PocomathInstance('cherry')
|
||||
cherry.install(numberAdd)
|
||||
|
Loading…
Reference in New Issue
Block a user