feat(PocomathInstance): Add bounded template parameters

This feature helps specify the return type of implementations where
  the return type depends on the exact subtype that the implementation
  was called with, such as negate.
This commit is contained in:
Glen Whitney 2022-08-22 12:38:23 -04:00
parent dc6921e768
commit f7bb3697ed
4 changed files with 77 additions and 65 deletions

View File

@ -2,7 +2,7 @@
import typed from 'typed-function'
import {makeChain} from './Chain.mjs'
import {dependencyExtractor, generateTypeExtractor} from './extractors.mjs'
import {R_, returnTypeOf} from './returns.mjs'
import {Returns, returnTypeOf} from './Returns.mjs'
import {typeListOfSignature, typesOfSignature, subsetOfKeys} from './utils.mjs'
const anySpec = {} // fixed dummy specification of 'any' type
@ -13,7 +13,9 @@ const templateFromParam = 'U' // For defining covariant conversions
/* Returns a new signature just like sig but with the parameter replaced by
* the type
*/
function substituteInSig(sig, parameter, type) {
const upperBounds = /\s*[(]=\s*(\w*)\s*/g
function substituteInSignature(signature, parameter, type) {
const sig = signature.replaceAll(upperBounds, '')
const pattern = new RegExp("\\b" + parameter + "\\b", 'g')
return sig.replaceAll(pattern, type)
}
@ -30,12 +32,14 @@ export default class PocomathInstance {
'install',
'installType',
'instantiateTemplate',
'isPriorTo',
'isSubtypeOf',
'joinTypes',
'name',
'returnTypeOf',
'self',
'subtypesOf',
'supertypesOf',
'Templates',
'typeOf',
'Types',
@ -88,7 +92,7 @@ export default class PocomathInstance {
this._installFunctions({
typeOf: {
ground: {uses: new Set(), does: () => R_('string', () => 'any')}
ground: {uses: new Set(), does: () => Returns('string', () => 'any')}
}
})
@ -202,7 +206,7 @@ export default class PocomathInstance {
}
if (!(this._typed.isTypedFunction(operation))) return 'any'
const details = this._typed.findSignature(operation, signature)
return returnTypeOf(details.fn)
return returnTypeOf(details.fn, signature, this)
}
/* Return a chain object for this instance with a given value: */
@ -398,7 +402,7 @@ export default class PocomathInstance {
}
// update the typeOf function
const imp = {}
imp[type] = {uses: new Set(), does: () => R_('string', () => type)}
imp[type] = {uses: new Set(), does: () => Returns('string', () => type)}
this._installFunctions({typeOf: imp})
}
@ -412,18 +416,36 @@ export default class PocomathInstance {
supSubs.splice(i, 0, sub)
}
/* Returns true is typeA is a subtype of type B */
/* Returns true if typeA is a subtype of type B */
isSubtypeOf(typeA, typeB) {
return this._subtypes[typeB].includes(typeA)
}
/* Returns true if typeA is a subtype of or converts to type B */
isPriorTo(typeA, typeB) {
if (!(typeB in this._priorTypes)) return false
return this._priorTypes[typeB].has(typeA)
}
/* Returns a list of the subtypes of a given type, in topological sorted
* order (i.e, no type on the list contains one that comes after it).
*/
subtypesOf(type) {
// HERE! For this to work, have to maintain subtypes as a sorted list.
return this._subtypes[type] // should we clone?
}
/* Returns a list of the supertypes of a given type, starting with itself,
* in topological order
*/
supertypesOf(type) {
const supList = []
while (type) {
supList.push(type)
type = this.Types[type].refines
}
return supList
}
/* Returns the most refined type containing all the types in the array,
* with '' standing for the empty type for convenience. If the second
* argument `convert` is true, a convertible type is considered a
@ -488,7 +510,7 @@ export default class PocomathInstance {
const imp = {}
imp[type] = {
uses: new Set(['T']),
does: ({T}) => R_('string', () => `${base}<${T}>`)
does: ({T}) => Returns('string', () => `${base}<${T}>`)
}
this._installFunctions({typeOf: imp})
@ -628,7 +650,7 @@ export default class PocomathInstance {
/* Check if it's an ordinary non-template signature */
let explicit = true
for (const type of typesOfSignature(rawSignature)) {
for (const word of type.split(/[<>]/)) {
for (const word of type.split(/[<>(=\s]/)) {
if (this._templateParam(word)) {
explicit = false
break
@ -640,14 +662,26 @@ export default class PocomathInstance {
continue
}
/* It's a template, have to instantiate */
/* First, find any upper bounds on the instantation */
/* TODO: handle multiple upper bounds */
upperBounds.lastIndex = 0
let ubType = upperBounds.exec(rawSignature)
if (ubType) {
ubType = ubType[1]
if (!ubType in this.Types) {
throw new TypeError(
`Unknown type upper bound '${ubType}' in '${rawSignature}'`)
}
}
/* First, add the known instantiations, gathering all types needed */
if (!('instantiations' in behavior)) {
behavior.instantiations = new Set()
if (ubType) behavior.instantiations.add(ubType)
}
let instantiationSet = new Set()
for (const instType of behavior.instantiations) {
instantiationSet.add(instType)
for (const other of this._priorTypes[instType]) {
for (const other of this.subtypesOf(instType)) {
instantiationSet.add(other)
}
}
@ -656,7 +690,7 @@ export default class PocomathInstance {
if (!(instType in this.Types)) continue
if (this.Types[instType] === anySpec) continue
const signature =
substituteInSig(rawSignature, theTemplateParam, instType)
substituteInSignature(rawSignature, theTemplateParam, instType)
/* Don't override an explicit implementation: */
if (signature in imps) continue
/* Don't go too deep */
@ -670,7 +704,7 @@ export default class PocomathInstance {
const uses = new Set()
for (const dep of behavior.uses) {
if (this._templateParam(dep)) continue
uses.add(substituteInSig(dep, theTemplateParam, instType))
uses.add(substituteInSignature(dep, theTemplateParam, instType))
}
const patch = (refs) => {
const innerRefs = {}
@ -678,7 +712,7 @@ export default class PocomathInstance {
if (this._templateParam(dep)) {
innerRefs[dep] = instType
} else {
const outerName = substituteInSig(
const outerName = substituteInSignature(
dep, theTemplateParam, instType)
innerRefs[dep] = refs[outerName]
}
@ -689,11 +723,13 @@ export default class PocomathInstance {
tf_imps, signature, {uses, does: patch})
}
/* Now add the catchall signature */
/* (Not needed if if it's a bounded template) */
if (ubType) continue
let templateCall = `<${theTemplateParam}>`
/* Relying here that the base of 'Foo<T>' is 'Foo': */
let baseSignature = rawSignature.replaceAll(templateCall, '')
/* Any remaining template params are top-level */
const signature = substituteInSig(
const signature = substituteInSignature(
baseSignature, theTemplateParam, 'ground')
/* The catchall signature has to detect the actual type of the call
* and add the new instantiations.
@ -718,7 +754,8 @@ export default class PocomathInstance {
for (const dep of behavior.uses) {
let [func, needsig] = dep.split(/[()]/)
if (needsig) {
const subsig = substituteInSig(needsig, theTemplateParam, '')
const subsig = substituteInSignature(
needsig, theTemplateParam, '')
if (subsig === needsig) {
simplifiedUses[dep] = dep
} else {
@ -789,7 +826,7 @@ export default class PocomathInstance {
self._maxDepthSeen = depth
}
/* Generate the list of actual wanted types */
const wantTypes = parTypes.map(type => substituteInSig(
const wantTypes = parTypes.map(type => substituteInSignature(
type, theTemplateParam, instantiateFor))
/* Now we have to add any actual types that are relevant
* to this invocation. Namely, that would be every formal parameter
@ -811,7 +848,7 @@ export default class PocomathInstance {
if (wantType.slice(0,3) === '...') {
wantType = wantType.slice(3)
}
wantType = substituteInSig(
wantType = substituteInSignature(
wantType, theTemplateParam, instantiateFor)
if (wantType !== parTypes[i]) {
args[j] = self._typed.convert(args[j], wantType)
@ -840,7 +877,7 @@ export default class PocomathInstance {
} else {
let [func, needsig] = dep.split(/[()]/)
if (self._typed.isTypedFunction(refs[simplifiedDep])) {
const subsig = substituteInSig(
const subsig = substituteInSignature(
needsig, theTemplateParam, instantiateFor)
let resname = simplifiedDep
if (resname == 'self') resname = name
@ -890,8 +927,8 @@ export default class PocomathInstance {
}
/* Adapts Pocomath-style behavior specification (uses, does) for signature
* to typed-function implementations and inserts the result into plain object
* imps
* to typed-function implementations and inserts the result into plain
* object imps
*/
_addTFimplementation(imps, signature, behavior) {
const {uses, does} = behavior
@ -910,7 +947,7 @@ export default class PocomathInstance {
* Verify that the desired signature has been fully grounded:
*/
if (needsig) {
const trysig = substituteInSig(needsig, theTemplateParam, '')
const trysig = substituteInSignature(needsig, theTemplateParam, '')
if (trysig !== needsig) {
throw new Error(
'Attempt to add a template implementation: ' +
@ -1051,8 +1088,9 @@ export default class PocomathInstance {
return resultingTypes
}
/* Maybe add the instantiation of template type base with argument tyoe
* instantiator to the Types of this instance, if it hasn't happened already.
/* Maybe add the instantiation of template type base with argument type
* instantiator to the Types of this instance, if it hasn't happened
* already.
* Returns the name of the type if added, false if it was already there,
* and undefined if the type is declined (because of being nested too deep).
*/
@ -1081,7 +1119,7 @@ export default class PocomathInstance {
if (template.before) {
for (const beforeTmpl of template.before) {
beforeTypes.push(
substituteInSig(beforeTmpl, theTemplateParam, instantiator))
substituteInSignature(beforeTmpl, theTemplateParam, instantiator))
}
}
if (beforeTypes.length > 0) {
@ -1090,7 +1128,7 @@ export default class PocomathInstance {
newTypeSpec.test = template.test(this._typeTests[instantiator])
if (template.from) {
for (let source in template.from) {
const instSource = substituteInSig(
const instSource = substituteInSignature(
source, theTemplateParam, instantiator)
let usesFromParam = false
for (const word of instSource.split(/[<>]/)) {
@ -1101,14 +1139,14 @@ export default class PocomathInstance {
}
if (usesFromParam) {
for (const iFrom in instantiatorSpec.from) {
const finalSource = substituteInSig(
const finalSource = substituteInSignature(
instSource, templateFromParam, iFrom)
maybeFrom[finalSource] = template.from[source](
instantiatorSpec.from[iFrom])
}
// Assuming all templates are covariant here, I guess...
for (const subType of this._subtypes[instantiator]) {
const finalSource = substituteInSig(
const finalSource = substituteInSignature(
instSource, templateFromParam, subType)
maybeFrom[finalSource] = template.from[source](x => x)
}

View File

@ -15,15 +15,20 @@ function cloneFunction(fn) {
return theClone
}
export function R_(type, fn) {
export function Returns(type, fn) {
if ('returns' in fn) fn = cloneFunction(fn)
fn.returns = type
return fn
}
export function returnTypeOf(fn) {
return fn.returns || 'any'
export function returnTypeOf(fn, signature, pmInstance) {
const typeOfReturns = typeof fn.returns
if (typeOfReturns === 'undefined') return 'any'
if (typeOfReturns === 'string') return fn.returns
// not sure if we will need a function to determine the return type,
// but allow it for now:
return fn.returns(signature, pmInstance)
}
export default R_
export default Returns

View File

@ -1,37 +1,6 @@
import R_ from '../core/returns.mjs'
import Returns from '../core/Returns.mjs'
export * from './Types/number.mjs'
export const negate = {
NumInt: () => R_('NumInt', n => -n),
number: () => R_('number', n => -n)
'T (= number': ({T}) => Returns(T, n => -n)
}
/* Can we find a mechanism to avoid reiterating the definition
for each number (sub)type? Maybe something like:
export const negate = {'T:number': ({T}) => R_(T, n => -n)}
where 'T:number' is a new notation that means "T is a (non-strict) subtype
of number". That doesn't look too bad, but if we went down that route, we
might also need to make sure that if we ever implemented a PositiveNumber
type and a NegativeNumber type then we could say
export const negate = {
PositiveNumber: () => R_('NegativeNumber', n => -n),
NegativeNumber: () => R_('PositiveNumber', n => -n),
'T:number': ({T}) => R_(T, n => -n)
}
This just gets worse if there are also PosNumInt and NegNumInt types;
maybe Positive<T>, Negative<T>, and Zero<T> are template types, so that
we could say:
export const negate = {
'Positive<T:number>': ({'Negative<T>': negT}) => R_(negT, n => -n),
'Negative<T:number>': ({'Positive<T>': posT}) => R_(posT, n => -n),
T:number: ({T}) => R_(T, n => -n)
}
But all of that is pretty speculative, for now let's just go with writing
out the different types.
*/

View File

@ -27,7 +27,7 @@ describe('The default full pocomath instance "math"', () => {
it('can subtract numbers', () => {
assert.strictEqual(math.subtract(12, 5), 7)
//assert.strictEqual(math.subtract(3n, 1.5), 1.5)
assert.throws(() => math.subtract(3n, 1.5), 'TypeError')
})
it('can add numbers', () => {