feat: Return type annotations (#53)

Provides the infrastructure to allow annotating the return types of functions, and does so for essentially every operation in the system (the only known exceptions being add, multiply, etc., on arbitrarily many arguments).

One main infrastructure enhancements are bounded template types, e.g. `T:number` being a template parameter where T can take on the type `number` or any subtype thereof.

A main internal enhancement is that base template types are no longer added to the typed universe; rather, there is a secondary, "meta" typed universe where they live. The primary point/purpose of this change is then the necessary search order for implementations can be much better modeled by typed-function's search order, using the `onMismatch` facility to redirect the search from fully instantiated implementations to the generic catchall implementations for each template (these catchalls live in the meta universe).

Numerous other small improvements and bugfixes were encountered along the way.

Co-authored-by: Glen Whitney <glen@studioinfinity.org>
Reviewed-on: #53
This commit is contained in:
Glen Whitney 2022-08-30 19:36:44 +00:00
parent 207ac4330b
commit 31add66f4c
80 changed files with 1502 additions and 606 deletions

View file

@ -1,15 +1,14 @@
import {Returns, returnTypeOf} from '../../core/Returns.mjs'
import PocomathInstance from '../../core/PocomathInstance.mjs'
const Complex = new PocomathInstance('Complex')
// Base type that should generally not be used directly
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
// in their component types.
// Now the template type: Complex numbers are actually always homogeneous
// in their component types. For an explanation of the meanings of the
// properties, see ../../tuple/Types/Tuple.mjs
Complex.installType('Complex<T>', {
infer: ({typeOf, joinTypes}) => z => joinTypes([typeOf(z.re), typeOf(z.im)]),
base: z => z && typeof z === 'object' && 're' in z && 'im' in z,
test: testT => z => testT(z.re) && testT(z.im),
infer: ({typeOf, joinTypes}) => z => joinTypes([typeOf(z.re), typeOf(z.im)]),
from: {
T: t => ({re: t, im: t-t}), // hack: maybe need a way to call zero(T)
U: convert => u => {
@ -21,7 +20,12 @@ Complex.installType('Complex<T>', {
})
Complex.promoteUnary = {
'Complex<T>': ({'self(T)': me, complex}) => z => complex(me(z.re), me(z.im))
'Complex<T>': ({
T,
'self(T)': me,
complex
}) => Returns(
`Complex<${returnTypeOf(me)}>`, z => complex(me(z.re), me(z.im)))
}
export {Complex}

View file

@ -1,10 +1,21 @@
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
T,
sqrt, // Unfortunately no notation yet for the needed signature
'absquare(T)': baseabsq,
'absquare(Complex<T>)': absq
}) => z => sqrt(absq(z))
}) => {
const midType = returnTypeOf(baseabsq)
const sqrtImp = sqrt.fromInstance.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,27 @@
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 midType = returnTypeOf(absq)
const addImp = add.fromInstance.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,8 +1,10 @@
import Returns from '../core/Returns.mjs'
export * from './Types/Complex.mjs'
export const add = {
'Complex<T>,Complex<T>': ({
T,
'self(T,T)': me,
'complex(T,T)': cplx
}) => (w,z) => cplx(me(w.re, z.re), me(w.im, z.im))
}) => Returns(`Complex<${T}>`, (w,z) => cplx(me(w.re, z.re), me(w.im, z.im)))
}

View file

@ -1,3 +1,4 @@
import Returns from '../core/Returns.mjs'
export * from './Types/Complex.mjs'
/* Returns true if w is z multiplied by a complex unit */
@ -9,9 +10,9 @@ export const associate = {
'one(T)': uno,
'complex(T,T)': cplx,
'negate(Complex<T>)': neg
}) => (w,z) => {
}) => Returns('boolean', (w,z) => {
if (eq(w,z) || eq(w,neg(z))) return true
const ti = times(z, cplx(zr(z.re), uno(z.im)))
return eq(w,ti) || eq(w,neg(ti))
}
})
}

View file

@ -1,3 +1,4 @@
import Returns from '../core/Returns.mjs'
export * from './Types/Complex.mjs'
export * from '../generic/Types/generic.mjs'
@ -6,15 +7,16 @@ export const complex = {
* have a numeric/scalar type, e.g. by implementing subtypes in
* typed-function
*/
'undefined': () => u => u,
'undefined,any': () => (u, y) => u,
'any,undefined': () => (x, u) => u,
'undefined,undefined': () => (u, v) => u,
'T,T': () => (x, y) => ({re: x, im: y}),
'undefined': () => Returns('undefined', u => u),
'undefined,any': () => Returns('undefined', (u, y) => u),
'any,undefined': () => Returns('undefined', (x, u) => u),
'undefined,undefined': () => Returns('undefined', (u, v) => u),
'T,T': ({T}) => Returns(`Complex<${T}>`, (x, y) => ({re: x, im: y})),
/* Take advantage of conversions in typed-function */
// 'Complex<T>': () => z => z
/* But help out because without templates built in to typed-function,
* type inference turns out to be too hard
*/
'T': ({'zero(T)': zr}) => x => ({re: x, im: zr(x)})
'T': ({T, 'zero(T)': zr}) => Returns(
`Complex<${T}>`, x => ({re: x, im: zr(x)}))
}

View file

@ -1,9 +1,11 @@
import Returns from '../core/Returns.mjs'
export * from './Types/Complex.mjs'
export const conjugate = {
'Complex<T>': ({
T,
'negate(T)': neg,
'complex(T,T)': cplx
}) => z => cplx(z.re, neg(z.im))
}) => Returns(`Complex<${T}>`, z => cplx(z.re, neg(z.im)))
}

View file

@ -1,9 +1,11 @@
import Returns from '../core/Returns.mjs'
export * from './Types/Complex.mjs'
export const equalTT = {
'Complex<T>,Complex<T>': ({
T,
'self(T,T)': me
}) => (w,z) => me(w.re, z.re) && me(w.im, z.im),
}) => Returns('boolean', (w, z) => me(w.re, z.re) && me(w.im, z.im)),
// NOTE: Although I do not understand exactly why, with typed-function@3.0's
// matching algorithm, the above template must come first to ensure the
// most specific match to a template call. I.e, if one of the below
@ -11,16 +13,16 @@ export const equalTT = {
// with (Complex<Complex<number>>, Complex<number>) (!, hopefully in some
// future iteration typed-function will be smart enough to prefer
// Complex<T>, Complex<T>. Possibly the problem is in Pocomath's bolted-on
// type resolution and the difficulty will go away when features are moved into
// typed-function.
// type resolution and the difficulty will go away when features are moved
// into typed-function.
'Complex<T>,T': ({
'isZero(T)': isZ,
'self(T,T)': eqReal
}) => (z, x) => eqReal(z.re, x) && isZ(z.im),
}) => Returns('boolean', (z, x) => eqReal(z.re, x) && isZ(z.im)),
'T,Complex<T>': ({
'isZero(T)': isZ,
'self(T,T)': eqReal
}) => (b, z) => eqReal(z.re, b) && isZ(z.im),
}) => Returns('boolean', (b, z) => eqReal(z.re, b) && isZ(z.im)),
}

View file

@ -1,5 +1,6 @@
import PocomathInstance from '../core/PocomathInstance.mjs'
import * as Complex from './Types/Complex.mjs'
import Returns from '../core/Returns.mjs'
import * as Complex from './Types/Complex.mjs'
import gcdType from '../generic/gcdType.mjs'
const gcdComplexRaw = {}
@ -9,15 +10,16 @@ const imps = {
gcdComplexRaw,
gcd: { // Only return gcds with positive real part
'Complex<T>,Complex<T>': ({
T,
'gcdComplexRaw(Complex<T>,Complex<T>)': gcdRaw,
'sign(T)': sgn,
'one(T)': uno,
'negate(Complex<T>)': neg
}) => (z,m) => {
}) => Returns(`Complex<${T}>`, (z,m) => {
const raw = gcdRaw(z, m)
if (sgn(raw.re) === uno(raw.re)) return raw
return neg(raw)
}
})
}
}

View file

@ -1,14 +1,16 @@
import Returns from '../core/Returns.mjs'
export * from './Types/Complex.mjs'
export const invert = {
'Complex<T>': ({
T,
'conjugate(Complex<T>)': conj,
'absquare(Complex<T>)': asq,
'complex(T,T)': cplx,
'divide(T,T)': div
}) => z => {
}) => Returns(`Complex<${T}>`, z => {
const c = conj(z)
const d = asq(z)
return cplx(div(c.re, d), div(c.im, d))
}
})
}

View file

@ -1,5 +1,7 @@
import Returns from '../core/Returns.mjs'
export * from './Types/Complex.mjs'
export const isZero = {
'Complex<T>': ({'self(T)': me}) => z => me(z.re) && me(z.im)
'Complex<T>': ({'self(T)': me}) => Returns(
'boolean', z => me(z.re) && me(z.im))
}

View file

@ -1,15 +1,20 @@
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,
'subtract(T,T)': subt,
'self(T,T)': me,
'conjugate(T)': conj // makes quaternion multiplication work
}) => (w,z) => {
return 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)))
}
}) => Returns(
`Complex<${T}>`,
(w,z) => {
const realpart = subt(me( w.re, z.re), me(conj(w.im), z.im))
const imagpart = plus(me(conj(w.re), z.im), me( w.im, z.re))
return cplx(realpart, imagpart)
}
)
}

View file

@ -1,5 +1,14 @@
import Returns from '../core/Returns.mjs'
export * from './Types/Complex.mjs'
// Might be nice to have type aliases!
export const quaternion = {
'T,T,T,T': ({complex}) => (r,i,j,k) => complex(complex(r,j), complex(i,k))
'T,T,T,T': ({
T,
'complex(T,T)': cplxT,
'complex(Complex<T>,Complex<T>)': quat
}) => Returns(
`Complex<Complex<${T}>>`,
(r,i,j,k) => quat(cplxT(r,j), cplxT(i,k))
)
}

View file

@ -1,7 +1,9 @@
import Returns from '../core/Returns.mjs'
export * from './roundquotient.mjs'
export const quotient = {
'Complex<T>,Complex<T>': ({
T,
'roundquotient(Complex<T>,Complex<T>)': rq
}) => (w,z) => rq(w,z)
}) => Returns(`Complex<${T}>`, (w,z) => rq(w,z))
}

View file

@ -1,17 +1,19 @@
import Returns from '../core/Returns.mjs'
export * from './Types/Complex.mjs'
export const roundquotient = {
'Complex<T>,Complex<T>': ({
T,
'isZero(Complex<T>)': isZ,
'conjugate(Complex<T>)': conj,
'multiply(Complex<T>,Complex<T>)': mult,
'absquare(Complex<T>)': asq,
'self(T,T)': me,
'complex(T,T)': cplx
}) => (n,d) => {
}) => Returns(`Complex<${T}>`, (n,d) => {
if (isZ(d)) return d
const cnum = mult(n, conj(d))
const dreal = asq(d)
return cplx(me(cnum.re, dreal), me(cnum.im, dreal))
}
})
}

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)
}
)
}
}