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

View File

@ -15,15 +15,20 @@ function cloneFunction(fn) {
return theClone return theClone
} }
export function R_(type, fn) { export function Returns(type, fn) {
if ('returns' in fn) fn = cloneFunction(fn) if ('returns' in fn) fn = cloneFunction(fn)
fn.returns = type fn.returns = type
return fn return fn
} }
export function returnTypeOf(fn) { export function returnTypeOf(fn, signature, pmInstance) {
return fn.returns || 'any' 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 * from './Types/number.mjs'
export const negate = { export const negate = {
NumInt: () => R_('NumInt', n => -n), 'T (= number': ({T}) => Returns(T, n => -n)
number: () => R_('number', 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', () => { it('can subtract numbers', () => {
assert.strictEqual(math.subtract(12, 5), 7) 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', () => { it('can add numbers', () => {