feat(return types): Add more return types for complex functions

These changes greatly increased the need for precision in generating
  implementations for signatures of operations whenever possible. So this
  commit also includes a refactor that basically caches all of the
  conversions of Pocomath implementations to typed-function implementatios so
  that they are more often externally available (i.e., not disrupted so much
  after invalidation).
This commit is contained in:
Glen Whitney 2022-08-26 23:54:32 -04:00
parent bc434c7163
commit a2f76a55b8
13 changed files with 273 additions and 98 deletions

View File

@ -3,7 +3,7 @@ export * from './Types/bigint.mjs'
export const divide = {
'bigint,bigint': ({config, 'quotient(bigint,bigint)': quot}) => {
if (config.predictable) return quot
if (config.predictable) return Returns('bigint', (n,d) => quot(n,d))
return Returns('bigint|undefined', (n, d) => {
const q = n/d
if (q * d == n) return q

View File

@ -6,7 +6,7 @@ const Complex = new PocomathInstance('Complex')
Complex.installType('Complex', {
test: z => z && typeof z === 'object' && 're' in z && 'im' in z
})
// Now the template type: Complex numbers are actually always homeogeneous
// Now the template type: Complex numbers are actually always homogeneous
// in their component types.
Complex.installType('Complex<T>', {
infer: ({typeOf, joinTypes}) => z => joinTypes([typeOf(z.re), typeOf(z.im)]),

View File

@ -1,10 +1,25 @@
import {Returns, returnTypeOf} from '../core/Returns.mjs'
export * from './Types/Complex.mjs'
export const abs = {
'Complex<T>': ({
sqrt, // Calculation of the type needed in the square root (the
// underlying numeric type of T, whatever T is, is beyond Pocomath's
// (current) template abilities, so punt and just do full resolution
sqrt, // Unfortunately no notation yet for the needed signature
'absquare(T)': baseabsq,
'absquare(Complex<T>)': absq
}) => z => sqrt(absq(z))
}) => {
const pm = sqrt.fromInstance
if (typeof pm === 'undefined') {
// Just checking for the dependencies, return value is irrelevant
return undefined
}
const midType = returnTypeOf(baseabsq)
const sqrtImp = pm.resolve('sqrt', midType, sqrt)
let retType = returnTypeOf(sqrtImp)
if (retType.includes('|')) {
// This is a bit of a hack, as it relies on all implementations of
// sqrt returning the "typical" return type as the first option
retType = retType.split('|',1)[0]
}
return Returns(retType, z => sqrtImp(absq(z)))
}
}

View File

@ -1,9 +1,31 @@
import {Returns, returnTypeOf} from '../core/Returns.mjs'
export * from './Types/Complex.mjs'
export const absquare = {
'Complex<T>': ({
add, // Calculation of exact type needed in add (underlying numeric of T)
// is (currently) too involved for Pocomath
add, // no easy way to write the needed signature; if T is number
// it is number,number; but if T is Complex<bigint>, it is just
// bigint,bigint. So unfortunately we depend on all of add, and
// we extract the needed implementation below.
'self(T)': absq
}) => z => add(absq(z.re), absq(z.im))
}) => {
const pm = add.fromInstance
if (typeof pm === 'undefined') {
// Just checking the dependencies, return value irrelevant
return undefined
}
const midType = returnTypeOf(absq)
const addImp = pm.resolve('add', `${midType},${midType}`, add)
return Returns(
returnTypeOf(addImp), z => addImp(absq(z.re), absq(z.im)))
}
}
/* We could imagine notations that Pocomath could support that would simplify
* the above, maybe something like
* 'Complex<T>': ({
* 'self(T): U': absq,
* 'add(U,U):V': plus,
* V
* }) => Returns(V, z => plus(absq(z.re), absq(z.im)))
*/

View File

@ -1,15 +1,18 @@
import Returns from '../core/Returns.mjs'
export * from './Types/Complex.mjs'
export const multiply = {
'Complex<T>,Complex<T>': ({
T,
'complex(T,T)': cplx,
'add(T,T)': plus,
'subtract(T,T)': sub,
'self(T,T)': me,
'conjugate(T)': conj // makes quaternion multiplication work
}) => (w,z) => {
return cplx(
}) => Returns(
`Complex<${T}>`,
(w,z) => cplx(
sub(me(w.re, z.re), me(conj(w.im), z.im)),
plus(me(conj(w.re), z.im), me(w.im, z.re)))
}
)
}

View File

@ -1,3 +1,4 @@
import {Returns, returnTypeOf} from '../core/Returns.mjs'
export * from './Types/Complex.mjs'
export const sqrt = {
@ -12,29 +13,41 @@ export const sqrt = {
'multiply(T,T)': mult,
'self(T)': me,
'divide(T,T)': div,
'abs(Complex<T>)': absC,
'absquare(Complex<T>)': absqC,
'subtract(T,T)': sub
}) => {
let baseReturns = returnTypeOf(me)
if (baseReturns.includes('|')) {
// Bit of a hack, because it is relying on other implementations
// to list the "typical" value of sqrt first
baseReturns = baseReturns.split('|', 1)[0]
}
if (config.predictable) {
return z => {
return Returns(`Complex<${baseReturns}>`, z => {
const reOne = uno(z.re)
if (isZ(z.im) && sgn(z.re) === reOne) return cplxU(me(z.re))
const reTwo = plus(reOne, reOne)
const myabs = me(absqC(z))
return cplxB(
mult(sgn(z.im), me(div(plus(absC(z),z.re), reTwo))),
me(div(sub(absC(z),z.re), reTwo))
mult(sgn(z.im), me(div(plus(myabs, z.re), reTwo))),
me(div(sub(myabs, z.re), reTwo))
)
})
}
return Returns(
`Complex<${baseReturns}>|${baseReturns}|undefined`,
z => {
const reOne = uno(z.re)
if (isZ(z.im) && sgn(z.re) === reOne) return me(z.re)
const reTwo = plus(reOne, reOne)
const myabs = me(absqC(z))
const reSqrt = me(div(plus(myabs, z.re), reTwo))
const imSqrt = me(div(sub(myabs, z.re), reTwo))
if (reSqrt === undefined || imSqrt === undefined) return undefined
return cplxB(mult(sgn(z.im), reSqrt), imSqrt)
}
}
return z => {
const reOne = uno(z.re)
if (isZ(z.im) && sgn(z.re) === reOne) return me(z.re)
const reTwo = plus(reOne, reOne)
const reSqrt = me(div(plus(absC(z),z.re), reTwo))
const imSqrt = me(div(sub(absC(z),z.re), reTwo))
if (reSqrt === undefined || imSqrt === undefined) return undefined
return cplxB(mult(sgn(z.im), reSqrt), imSqrt)
}
)
}
}

View File

@ -38,6 +38,7 @@ export default class PocomathInstance {
'joinTypes',
'name',
'returnTypeOf',
'resolve',
'self',
'subtypesOf',
'supertypesOf',
@ -49,7 +50,8 @@ export default class PocomathInstance {
constructor(name) {
this.name = name
this._imps = {}
this._imps = {} // Pocomath implementations, with dependencies
this._TFimps = {} // typed-function implementations, dependencies resolved
this._affects = {}
this._typed = typed.create()
this._typed.clear()
@ -220,7 +222,6 @@ export default class PocomathInstance {
if (details) {
return returnTypeOf(details.fn, signature, this)
}
console.log('Checking return type of', operation)
return returnTypeOf(this[operation], signature, this)
})
@ -564,8 +565,28 @@ export default class PocomathInstance {
`Conflicting definitions of ${signature} for ${name}`)
}
} else {
// Must avoid aliasing into another instance:
opImps[signature] = {uses: behavior.uses, does: behavior.does}
/* Check if it's an ordinary non-template signature */
let explicit = true
for (const type of typesOfSignature(signature)) {
for (const word of type.split(/[<>:\s]/)) {
if (this._templateParam(word)) {
explicit = false
break
}
}
if (!explicit) break
}
opImps[signature] = {
explicit,
uses: behavior.uses,
does: behavior.does
}
if (explicit) {
opImps[signature].resolved = false
} else {
opImps[signature].hasInstantiations = {}
opImps[signature].needsInstantiations = new Set()
}
for (const dep of behavior.uses) {
const depname = dep.split('(', 1)[0]
if (depname === 'self' || this._templateParam(depname)) {
@ -604,11 +625,40 @@ export default class PocomathInstance {
* Reset an operation to require creation of typed-function,
* and if it has no implementations so far, set them up.
*/
_invalidate(name) {
if (this._invalid.has(name)) return
_invalidate(name, reason) {
if (!(name in this._imps)) {
this._imps[name] = {}
}
if (reason) {
// Make sure no TF imps that depend on reason remain:
for (const [signature, behavior] of Object.entries(this._imps[name])) {
let invalidated = false
if (reason.charAt(0) === ':') {
const badType = reason.slice(1)
if (signature.includes(badType)) invalidated = true
} else {
for (const dep of behavior.uses) {
if (dep.includes(reason)) {
invalidated = true
break
}
}
}
if (invalidated) {
if (behavior.explicit) {
if (behavior.resolved) delete this._TFimps[signature]
behavior.resolved = false
} else {
for (const fullSig
of Object.values(behavior.hasInstantiations)) {
delete this._TFimps[fullSig]
}
behavior.hasInstantiations = {}
}
}
}
}
if (this._invalid.has(name)) return
this._invalid.add(name)
this._invalidateDependents(name)
const self = this
@ -628,7 +678,7 @@ export default class PocomathInstance {
_invalidateDependents(name) {
if (name in this._affects) {
for (const ancestor of this._affects[name]) {
this._invalidate(ancestor)
this._invalidate(ancestor, name)
}
}
}
@ -642,6 +692,10 @@ export default class PocomathInstance {
if (!imps) {
throw new SyntaxError(`No implementations for ${name}`)
}
if (!(this._TFimps[name])) {
this._TFimps[name] = {}
}
const tf_imps = this._TFimps[name]
/* Collect the entries we know the types for */
const usableEntries = []
for (const entry of Object.entries(imps)) {
@ -664,20 +718,13 @@ export default class PocomathInstance {
* in the midst of being reassembled
*/
Object.defineProperty(this, name, {configurable: true, value: 'limbo'})
const tf_imps = {}
for (const [rawSignature, behavior] of usableEntries) {
/* Check if it's an ordinary non-template signature */
let explicit = true
for (const type of typesOfSignature(rawSignature)) {
for (const word of type.split(/[<>:\s]/)) {
if (this._templateParam(word)) {
explicit = false
break
}
if (behavior.explicit) {
if (!(behavior.resolved)) {
this._addTFimplementation(tf_imps, rawSignature, behavior)
tf_imps[rawSignature]._pocoSignature = rawSignature
behavior.resolved = true
}
}
if (explicit) {
this._addTFimplementation(tf_imps, rawSignature, behavior)
continue
}
/* It's a template, have to instantiate */
@ -693,12 +740,9 @@ export default class PocomathInstance {
}
}
/* First, add the known instantiations, gathering all types needed */
if (!('instantiations' in behavior)) {
behavior.instantiations = new Set()
if (ubType) behavior.instantiations.add(ubType)
}
if (ubType) behavior.needsInstantiations.add(ubType)
let instantiationSet = new Set()
for (const instType of behavior.instantiations) {
for (const instType of behavior.needsInstantiations) {
instantiationSet.add(instType)
const otherTypes =
ubType ? this.subtypesOf(instType) : this._priorTypes[instType]
@ -710,6 +754,7 @@ export default class PocomathInstance {
for (const instType of instantiationSet) {
if (!(instType in this.Types)) continue
if (this.Types[instType] === anySpec) continue
if (instType in behavior.hasInstantiations) continue
const signature =
substituteInSignature(rawSignature, theTemplateParam, instType)
/* Don't override an explicit implementation: */
@ -742,10 +787,14 @@ export default class PocomathInstance {
}
this._addTFimplementation(
tf_imps, signature, {uses, does: patch})
tf_imps[signature]._pocoSignature = rawSignature
tf_imps[signature]._pocoInstance = instType
behavior.hasInstantiations[instType] = signature
}
/* Now add the catchall signature */
/* (Not needed if if it's a bounded template) */
if (ubType) continue
if ('_catchall_' in behavior.hasInstantiations) continue
let templateCall = `<${theTemplateParam}>`
/* Relying here that the base of 'Foo<T>' is 'Foo': */
let baseSignature = rawSignature.replaceAll(templateCall, '')
@ -886,11 +935,11 @@ export default class PocomathInstance {
/* Arrange that the desired instantiation will be there next
* time so we don't have to go through that again for this type
*/
refs[theTemplateParam] = instantiateFor
behavior.instantiations.add(instantiateFor)
behavior.needsInstantiations.add(instantiateFor)
self._invalidate(name)
// And update refs because we now know the type we're instantiating
// for:
refs[theTemplateParam] = instantiateFor
const innerRefs = {}
for (const dep in simplifiedUses) {
const simplifiedDep = simplifiedUses[dep]
@ -903,7 +952,7 @@ export default class PocomathInstance {
needsig, theTemplateParam, instantiateFor)
let resname = simplifiedDep
if (resname == 'self') resname = name
innerRefs[dep] = self._pocoresolve(
innerRefs[dep] = self.resolve(
resname, subsig, refs[simplifiedDep])
} else {
innerRefs[dep] = refs[simplifiedDep]
@ -913,13 +962,15 @@ export default class PocomathInstance {
// Finally ready to make the call.
const implementation = behavior.does(innerRefs)
// We can access return type information here
// And in particular, if it's a template, we should try to
// And in particular, if it might be a template, we should try to
// instantiate it:
const returnType = returnTypeOf(implementation, wantSig, self)
const instantiated = self._maybeInstantiate(returnType)
if (instantiated) {
const tempBase = instantiated.split('<',1)[0]
self._invalidateDependents(':' + tempBase)
for (const possibility of returnType.split('|')) {
const instantiated = self._maybeInstantiate(possibility)
if (instantiated) {
const tempBase = instantiated.split('<',1)[0]
self._invalidateDependents(':' + tempBase)
}
}
return implementation(...args)
}
@ -932,6 +983,7 @@ export default class PocomathInstance {
const outerUses = new Set(Object.values(simplifiedUses))
this._addTFimplementation(
tf_imps, signature, {uses: outerUses, does: patch})
behavior.hasInstantiations._catchall_ = rawSignature
}
this._correctPartialSelfRefs(name, tf_imps)
// Make sure we have all of the needed (template) types; and if they
@ -946,9 +998,17 @@ export default class PocomathInstance {
}
}
for (const badSig of badSigs) {
const imp = tf_imps[badSig]
delete tf_imps[badSig]
const fromBehavior = this._imps[name][imp._pocoSignature]
if (fromBehavior.explicit) {
fromBehavior.resolved = false
} else {
delete fromBehavior.hasInstantiations[imp._pocoInstance]
}
}
const tf = this._typed(name, tf_imps)
Object.defineProperty(tf, 'fromInstance', {value: this})
Object.defineProperty(this, name, {configurable: true, value: tf})
return tf
}
@ -1006,13 +1066,20 @@ export default class PocomathInstance {
}
if (func === 'self') {
if (needsig) {
if (full_self_referential) {
throw new SyntaxError(
'typed-function does not support mixed full and '
+ 'partial self-reference')
}
if (subsetOfKeys(typesOfSignature(needsig), this.Types)) {
part_self_references.push(needsig)
/* Maybe we can resolve the self reference without troubling
* typed-function:
*/
if (needsig in imps && typeof imps[needsig] == 'function') {
refs[dep] = imps[needsig]
} else {
if (full_self_referential) {
throw new SyntaxError(
'typed-function does not support mixed full and '
+ 'partial self-reference')
}
if (subsetOfKeys(typesOfSignature(needsig), this.Types)) {
part_self_references.push(needsig)
}
}
} else {
if (part_self_references.length) {
@ -1024,19 +1091,41 @@ export default class PocomathInstance {
}
} else {
if (this[func] === 'limbo') {
/* We are in the midst of bundling func, so have to use
* an indirect reference to func. And given that, there's
* really no helpful way to extract a specific signature
/* We are in the midst of bundling func */
let fallback = true
/* So the first thing we can do is try the tf_imps we are
* accumulating:
*/
const self = this
refs[dep] = function () { // is this the most efficient?
return self[func].apply(this, arguments)
if (needsig) {
const tempTF = this._typed('dummy_' + func, this._TFimps[func])
let result = undefined
try {
result = this._typed.find(tempTF, needsig, {exact: true})
} catch {}
if (result) {
refs[dep] = result
fallback = false
}
}
if (fallback) {
/* Either we need the whole function or the signature
* we need is not available yet, so we have to use
* an indirect reference to func. And given that, there's
* really no helpful way to extract a specific signature
*/
const self = this
const redirect = function () { // is this the most efficient?
return self[func].apply(this, arguments)
}
Object.defineProperty(redirect, 'name', {value: func})
Object.defineProperty(redirect, 'fromInstance', {value: this})
refs[dep] = redirect
}
} else {
// can bundle up func, and grab its signature if need be
let destination = this[func]
if (destination && needsig) {
destination = this._pocoresolve(func, needsig)
destination = this.resolve(func, needsig)
}
refs[dep] = destination
}
@ -1076,7 +1165,8 @@ export default class PocomathInstance {
_correctPartialSelfRefs(name, imps) {
for (const aSignature in imps) {
if (!(imps[aSignature].deferred)) continue
const part_self_references = imps[aSignature].psr
const deferral = imps[aSignature]
const part_self_references = deferral.psr
const corrected_self_references = []
for (const neededSig of part_self_references) {
// Have to find a match for neededSig among the other signatures
@ -1098,8 +1188,8 @@ export default class PocomathInstance {
+ `${name}(${neededSig})`)
}
}
const refs = imps[aSignature].builtRefs
const does = imps[aSignature].sigDoes
const refs = deferral.builtRefs
const does = deferral.sigDoes
imps[aSignature] = this._typed.referTo(
...corrected_self_references, (...impls) => {
for (let i = 0; i < part_self_references.length; ++i) {
@ -1110,6 +1200,8 @@ export default class PocomathInstance {
return implementation
}
)
imps[aSignature]._pocoSignature = deferral._pocoSignature
imps[aSignature]._pocoInstance = deferral._pocoInstance
}
}
@ -1282,16 +1374,30 @@ export default class PocomathInstance {
typedFunction = this[name]
}
let result = undefined
if (!this._typed.isTypedFunction(typedFunction)) {
return result
const haveTF = this._typed.isTypedFunction(typedFunction)
if (haveTF) {
try {
result = this._typed.findSignature(typedFunction, sig, {exact: true})
} catch {
}
}
try {
result = this._typed.findSignature(typedFunction, sig, {exact: true})
} catch {
}
if (result) return result
if (result || !(this._imps[name])) return result
const foundsig = this._findSubtypeImpl(name, this._imps[name], sig)
if (foundsig) return this._typed.findSignature(typedFunction, foundsig)
if (foundsig) {
if (haveTF) {
return this._typed.findSignature(typedFunction, foundsig)
}
// We have an implementation but not a typed function. Do the best
// we can:
const foundImpl = this._imps[name][foundsig]
const needs = {}
for (const dep of foundImpl.uses) {
const [base, sig] = dep.split('()')
needs[dep] = this.resolve(base, sig)
}
const pseudoImpl = foundImpl.does(needs)
return {fn: pseudoImpl, implementation: pseudoImpl}
}
const wantTypes = typeListOfSignature(sig)
for (const [implSig, details]
of typedFunction._typedFunctionData.signatureMap) {
@ -1314,7 +1420,12 @@ export default class PocomathInstance {
return result
}
_pocoresolve(name, sig, typedFunction) {
/* Returns a function that implements the operation with the given name
* when called with the given signature. The optional third argument is
* the typed function that provides the operation name, which can be
* passed in for efficiency if it is already available.
*/
resolve = Returns('function', function (name, sig, typedFunction) {
if (!this._typed.isTypedFunction(typedFunction)) {
typedFunction = this[name]
}
@ -1323,6 +1434,6 @@ export default class PocomathInstance {
// total punt, revert to typed-function resolution on every call;
// hopefully this happens rarely:
return typedFunction
}
})
}

View File

@ -1,6 +1,9 @@
import Returns from '../core/Returns.mjs'
export const absquare = {
T: ({
T,
'square(T)': sq,
'abs(T)': abval
}) => t => sq(abval(t))
}) => Returns(T, t => sq(abval(t)))
}

View File

@ -1,3 +1,6 @@
import {Returns, returnTypeOf} from '../core/Returns.mjs'
export const square = {
T: ({'multiply(T,T)': multT}) => x => multT(x,x)
T: ({'multiply(T,T)': multT}) => Returns(
returnTypeOf(multT), x => multT(x,x))
}

View File

@ -1,6 +1,7 @@
import Returns from '../core/Returns.mjs'
export * from './Types/number.mjs'
/* Absolute value squared */
export const absquare = {
number: ({'square(number)': sqn}) => n => sqn(n)
'T:number': ({T, 'square(T)': sqn}) => Returns(T, n => sqn(n))
}

View File

@ -1,3 +1,5 @@
import Returns from '../core/Returns.mjs'
export * from './Types/number.mjs'
export const multiply = {'number,number': () => (m,n) => m*n}
export const multiply = {'T:number,T': ({T}) => Returns(T, (m,n) => m*n)}

View File

@ -1,3 +1,4 @@
import Returns from '../core/Returns.mjs'
export * from './Types/number.mjs'
export const sqrt = {
@ -5,13 +6,13 @@ export const sqrt = {
config,
'complex(number,number)': cplx,
'negate(number)': neg}) => {
if (config.predictable || !cplx) {
return n => isNaN(n) ? NaN : Math.sqrt(n)
if (config.predictable || !cplx) {
return Returns('number', n => isNaN(n) ? NaN : Math.sqrt(n))
}
return Returns('number|Complex<number>', n => {
if (isNaN(n)) return NaN
if (n >= 0) return Math.sqrt(n)
return cplx(0, Math.sqrt(neg(n)))
})
}
return n => {
if (isNaN(n)) return NaN
if (n >= 0) return Math.sqrt(n)
return cplx(0, Math.sqrt(neg(n)))
}
}
}

View File

@ -83,6 +83,7 @@ describe('complex', () => {
assert.deepStrictEqual(
math.multiply(q0, math.quaternion(2, 1, 0.1, 0.1)),
math.quaternion(1.9, 1.1, 2.1, -0.9))
math.absquare(math.complex(1.25, 2.5)) //HACK: need absquare(Complex<number>)
assert.strictEqual(math.abs(q0), Math.sqrt(2))
assert.strictEqual(math.abs(q1), Math.sqrt(33)/4)
})