feat: Narrow tsc typing of operation dependencies/implementations

This commit is contained in:
Glen Whitney 2024-09-29 13:48:06 -07:00
parent 90b66dc863
commit f575582879
19 changed files with 912 additions and 111 deletions

2
src/Complex/all.ts Normal file
View file

@ -0,0 +1,2 @@
export * as type_data from './type'
export * as arithmetic_functions from './arithmetic'

152
src/Complex/arithmetic.ts Normal file
View file

@ -0,0 +1,152 @@
import {Complex} from './type.js'
import {implementations, commonSpecs} from '@/core/Dispatcher'
import type {RawDependencies} from '@/core/Dispatcher'
import {commonSignature, RealType} from '@/interfaces/type'
import type {CommonSignature, CommonReturn} from '@/interfaces/type'
// Narrowly typed signature selectors, for the operations we need to use
// with atypical signatures:
const add = 'add' as const
const divide = 'divide' as const
export default function <T>() {
const baseSignature = commonSpecs<T>()
const withComplex
= <RD extends RawDependencies>(rd: RD) => baseSignature({
...rd, complex: {}
})
const realSignature = commonSpecs<RealType<T>>()
return implementations<CommonSignature<Complex<T>>>()
.dependent(withComplex({add: {}}), {
add: dep => (w, z) =>
dep.complex(dep.add(w.re, z.re), dep.add(w.im, z.im))
})
.dependent(withComplex({unaryMinus: {}}), {
unaryMinus: dep => z =>
dep.complex(dep.unaryMinus(z.re), dep.unaryMinus(z.im))
})
.dependent(withComplex({unaryMinus: {}, conj: {}}), {
conj: dep => z => dep.complex(dep.conj(z.re), dep.unaryMinus(z.im))
})
.dependent(withComplex({subtract: {}}), {
subtract: dep => (w, z) =>
dep.complex(dep.subtract(w.re, z.re), dep.subtract(w.re, z.re))
})
.dependent(withComplex({add: {}, subtract: {}, multiply: {}, conj: {}}), {
multiply: dep => (w, z) => {
const mult = dep.multiply
const realpart = dep.subtract(
mult( w.re, z.re), mult(dep.conj(w.im), z.im))
const imagpart = dep.add(
mult(dep.conj(w.re), z.im), mult( w.im, z.re))
return dep.complex(realpart, imagpart)
}
})
.dependent(baseSignature({
absquare: {},
add: {sig: commonSignature<'add', CommonReturn<'absquare', T>>()}
}), {
absquare: dep => z => dep.add(dep.absquare(z.re), dep.absquare(z.im))
})
.dependent({
conj: {},
absquare: {},
divideReal: {
is: divide,
sig: (a: Complex<T>, b: RealType<T>) => ({} as Complex<T>)
}
}, {
reciprocal: dep => z => dep.divideReal(dep.conj(z), dep.absquare(z))
})
.dependent({multiply: {}, reciprocal: {}}, {
divide: dep => (w,z) => dep.multiply(w, dep.reciprocal(z))
})
// The dependencies are tricky in the implementation of `sqrt` below,
// because there are three types involved: Complex<T>, T, and
// RealType<T>, all of which might be different.
// We have to get it straight which operations we need on each type;
// for example, we need `add` on three different combinations:
.dependent({
absquare: {}, re: {}, // Complex<T>-dependencies
...withComplex({zero: {}}), // T-dependencies
// And RealType<T>-dependencies:
...realSignature({
conservativeSqrt: {}, equal: {}, unaryMinus: {}, isnan: {}
}),
// And now mixed dependencies:
divideReal: {
is: divide,
sig: (a: Complex<T>, b: RealType<T>) => ({} as Complex<T>)
},
addRR: {
is: add,
sig: (a: RealType<T>, b: RealType<T>) => ({} as RealType<T>)
},
addTR: {is: add, sig: (a: T, b: RealType<T>) => ({} as T)},
addCR: {
is: add,
sig: (a: Complex<T>, b: RealType<T>) => ({} as Complex<T>)
}
}, {
sqrt: dep => z => {
const absq = dep.absquare(z)
const myabs = dep.conservativeSqrt(absq)
if (dep.isnan(myabs)) {
throw new RangeError(
`sqrt(${z}): cannot take square root of norm square ${absq}`
)
}
const r = dep.re(z)
const negr = dep.unaryMinus(r)
if (dep.equal(myabs, negr)) {
// pure imaginary square root; z.im already zero
const rootNegr = dep.conservativeSqrt(negr)
if (dep.isnan(rootNegr)) {
throw new RangeError(
`sqrt(${z}): cannot take square root of `
+ `negative real part ${negr}`
)
}
return dep.complex(
dep.zero(z.re), dep.addTR(z.im, rootNegr))
}
const num = dep.addCR(z, myabs)
const denomsq = dep.addRR(dep.addRR(myabs, myabs), dep.addRR(r, r))
const denom = dep.conservativeSqrt(denomsq)
if (dep.isnan(denom)) {
throw new RangeError(
`sqrt(z) for z = ${z}: cannot take square root of `
+ `2|z| + 2re(z) = ${denomsq}`
)
}
return dep.divideReal(num, denom)
}
})
.ship()
}
// Additional implementations for non-uniform signatures
export function mixed<T>() {
return implementations<{
add: (z: Complex<T>, r: RealType<T>) => Complex<T>,
divide: (z: Complex<T>, r: RealType<T>) => Complex<T>,
complex: CommonSignature<T>['complex']
}>()
.dependent({
addTR: {is: add, sig: (a: T, b: RealType<T>) => ({} as T)},
complex: {}
}, {
add: dep => (z, r) => dep.complex(dep.addTR(z.re, r), z.im)
})
.dependent({
divTR: {is: divide, sig: (a: T, b: RealType<T>) => ({} as T)},
complex: {}
}, {
divide: dep => (z, r) =>
dep.complex(dep.divTR(z.re, r), dep.divTR(z.im, r))
})
.ship()
}

71
src/Complex/type.ts Normal file
View file

@ -0,0 +1,71 @@
import {implementations, commonSpecs} from '@/core/Dispatcher'
import {joinTypes} from '@/core/type'
import type {
CommonInterface, CommonSignature, NaNType, OneType, ZeroType
} from '@/interfaces/type'
export type Complex<T> = {re: T, im: T}
export const Complex_type = {
name: 'Complex',
test: <T>(dep: {testT: (z: unknown) => z is T}) =>
(z: unknown): z is Complex<T> =>
typeof z === 'object' && z != null && 're' in z && 'im' in z
&& dep.testT(z.re) && dep.testT(z.im),
infer: (dep: {typeOf: CommonSignature<undefined>['typeOf']}) =>
(z: Complex<unknown>) => joinTypes(dep.typeOf(z.re), dep.typeOf(z.im)),
from: {
Complex: <U,T>(dep: {convert: CommonSignature<U,T>['convert']}) =>
(z: Complex<U>) => ({re: dep.convert(z.re), im: dep.convert(z.im)}),
T: <T>(dep: {zero: CommonSignature<T>['zero']}) =>
(t: T) => ({re: t, im: dep.zero(t)})
}
}
declare module "@/interfaces/type" {
interface AssociatedTypes<T> {
Complex: T extends Complex<infer R> ? {
type: Complex<R>
zero: Complex<ZeroType<R>>
one: Complex<OneType<R> | ZeroType<R>>
nan: Complex<NaNType<R>>
real: RealType<R>
closure: T
} : never
}
interface CommonInterface<T, Aux> {
complex: (re: T, im?: T) => Complex<T>
}
}
// internal builder
const cplex = <T>(a:T, b:T): Complex<T> => ({re: a, im: b})
export function lift<T>() {
return implementations<CommonSignature<T>>()
.dependent({zero: {}}, {
complex: dep => (a, b) => cplex(a, b || dep.zero(a))
}).ship()
}
export default function <T>() {
const baseSignature = commonSpecs<T>()
return implementations<CommonSignature<Complex<T>>>()
.dependent(baseSignature({zero: {}}), {
zero: dep => z => cplex(dep.zero(z.re), dep.zero(z.re))
})
.dependent(baseSignature({zero: {}, one: {}}), {
one: dep => z =>
cplex<OneType<T> | ZeroType<T>>(dep.one(z.re), dep.zero(z.re))
})
.dependent(baseSignature({nan: {}}), {
nan: dep => z => cplex(dep.nan(z.re), dep.nan(z.re))
})
.dependent(baseSignature({re: {}}), {
re: dep => z => dep.re(z.re)
})
.ship()
}

2
src/all.ts Normal file
View file

@ -0,0 +1,2 @@
export * as numbers from '@/numbers/all'
export * as Complex from '@/Complex/all'

View file

@ -0,0 +1,4 @@
export type Configuration = {
epsilon: number
predictable: boolean
}

201
src/core/Dispatcher.ts Normal file
View file

@ -0,0 +1,201 @@
import type {AnyFunc, CommonSignature, GenSigs} from '@/interfaces/type'
// A base type that roughly describes the dependencies of a single factory
// for implementations of one operation. It is an object whose keys are the
// identifiers are dependencies, and whose values describe that dependency.
// In the value for a given key, the 'is' property gives the name of the
// operation that dependency should be an instance of, defaulting to the key
// itself when not present, and the 'sig' property gives the desired
// signature for that operation. When the 'sig' property is not present,
// the signature will default to some ambient ensemble of signatures.
export type RawDependencies = Record<string, {is?: string, sig?: AnyFunc}>
// The following type transform fills in any unspecified signatures in RD
// with the corresponding signatures from SomeSigs:
type PatchedDepSpec<RD extends RawDependencies, SomeSigs extends GenSigs> = {
[K in keyof RD]: RD[K] extends {sig: AnyFunc}
? RD[K]
: K extends keyof SomeSigs ? (RD[K] & {sig: SomeSigs[K]}) : RD[K]
}
// A factory for building dependency specifications from the ensemble of
// common signatures for a specific type (and perhaps auxiliary type). This
// is typically used when describing implementation factories for one type
// that depend on the common signatures for a *different* type.
export function commonSpecs<
T,
Aux = T,
CommonSigs extends GenSigs = CommonSignature<T, Aux>
>() {
return <RD extends RawDependencies>(
rd: RD
): PatchedDepSpec<RD, CommonSigs> => Object.fromEntries(
Object.keys(rd).map(k => [
k, 'sig' in rd[k] ? rd[k]
: {...rd[k], sig: (() => undefined)}
])
) as PatchedDepSpec<RD, CommonSigs>
}
// Further constraint on a dependency specification that means it is ready
// to use with a given set of signatures:
type DepSpec<Signatures extends GenSigs, Needs extends string>
= {
[K in Needs]: K extends keyof Signatures
? {sig?: AnyFunc}
: {is: keyof Signatures, sig: AnyFunc}
}
// Just checks if an RawDependencies is really a DepSpec, and blanks it out if not
type DepCheck<
RD extends RawDependencies,
Signatures extends GenSigs,
Needs extends string = keyof RD & string
> = RD extends DepSpec<Signatures, Needs> ? RD
: {
[K in Needs]: K extends keyof Signatures ? {}
: {is: never, sig: (q: boolean) => void}
}
// The actual type of a dependency, given a dependency specification
type DepType<
Signatures extends GenSigs,
DS extends DepSpec<Signatures, (keyof DS & string)>
> = {[K in keyof DS]: DS[K] extends {sig: AnyFunc}
? DS[K]['sig']
: K extends keyof Signatures ? Signatures[K] : never
}
// A collection of dependency specifications for some of the operations in
// an ensemble of Signatures:
type Specifications<
Signatures extends GenSigs,
NeedKeys extends keyof Signatures & string,
NeedList extends Record<NeedKeys, string>
> = {[K in NeedKeys]: DepSpec<Signatures, NeedList[K]>}
// The type of a factory function for implementations of a dependent operation,
// given a dependency specification:
type FactoryType<
Signatures extends GenSigs,
K extends (keyof Signatures) & string,
DS extends DepSpec<Signatures, (keyof DS & string)>
> = (dep: DepType<Signatures, DS>) => Signatures[K]
// The type of an implementation specification for an operation given its
// dependency specification: either directly the implementation if there
// are actually no dependencies, or a factory function and collection of
// dependency names otherwise:
type ImpType<
Signatures extends GenSigs,
K extends (keyof Signatures) & string,
DS extends DepSpec<Signatures, (keyof DS & string)>
> = DS extends null ? {implementation: Signatures[K]}
: {factory: FactoryType<Signatures, K, DS>, dependencies: DS}
// A collection of implementations for some operations of an ensemble of
// Signatures, matching a given collection of dependency specifications
type Implementations<
Signatures extends GenSigs,
NeedKeys extends keyof Signatures & string,
NeedList extends Record<NeedKeys, string>,
Specs extends Specifications<Signatures, NeedKeys, NeedList>
> = {[K in NeedKeys]: ImpType<Signatures, K, Specs[K]>}
// The builder interface that lets us assemble narrowly-typed Implementations:
interface ImplementationBuilder<
Signatures extends GenSigs,
NeedKeys extends keyof Signatures & string,
NeedList extends Record<NeedKeys, string>,
Specs extends Specifications<Signatures, NeedKeys, NeedList>
> {
independent<NewKeys extends (keyof Signatures) & string>(
independentImps: {[K in NewKeys]: Signatures[K]}
): ImplementationBuilder<
Signatures,
NeedKeys | NewKeys,
NeedList & {[K in NewKeys]: never},
Specs & {[K in NewKeys]: null}
>
dependent<
RD extends RawDependencies, // Easier to infer
NewKeys extends (keyof Signatures) & string,
DepKeys extends string = keyof RD & string
>(
depSpec: RD,
imps: {
[K in NewKeys]:
FactoryType<Signatures, K, DepCheck<RD, Signatures>>
}
): ImplementationBuilder<
Signatures,
NeedKeys | NewKeys,
NeedList & {[K in NewKeys]: DepKeys},
Specs & {[K in NewKeys]: DepCheck<RD, Signatures>}
>
ship(): Implementations<Signatures, NeedKeys, NeedList, Specs>
}
// And a function that actually provides the builder interface:
function impBuilder<
Signatures extends GenSigs,
NeedKeys extends keyof Signatures & string,
NeedList extends Record<NeedKeys, string>,
Specs extends Specifications<Signatures, NeedKeys, NeedList>
>(
sofar: Implementations<Signatures, NeedKeys, NeedList, Specs>
): ImplementationBuilder<Signatures, NeedKeys, NeedList, Specs> {
return {
independent<NewKeys extends (keyof Signatures) & string>(
imps: {[K in NewKeys]: Signatures[K]}) {
return impBuilder({
...sofar,
...Object.fromEntries(Object.keys(imps).map(k => [k, {
implementation: imps[k]
}]))
} as Implementations<
Signatures,
NeedKeys | NewKeys,
NeedList & {[K in NewKeys]: never},
Specs & {[K in NewKeys]: null}
>)
},
dependent<
RD extends RawDependencies,
NewKeys extends (keyof Signatures) & string,
DepKeys extends string = keyof RD & string
>(
depSpec: RD,
imps: {
[K in NewKeys]:
FactoryType<Signatures, K, DepCheck<RD, Signatures, DepKeys>>
}
) {
return impBuilder({
...sofar,
...Object.fromEntries(Object.keys(imps).map(k => [k, {
factory: imps[k],
dependencies: depSpec
}]))
}) as unknown as ImplementationBuilder<
Signatures,
NeedKeys | NewKeys,
NeedList & {[K in NewKeys]: DepKeys},
Specs & {[K in NewKeys]: DepCheck<RD, Signatures, DepKeys>}
>
},
ship() {
return (sofar as
Implementations<Signatures, NeedKeys, NeedList, Specs>)
}
}
}
// A convenience function that gives you an implementation builder:
export function implementations<Signatures extends GenSigs>(
): ImplementationBuilder<Signatures, never, {}, {}> {
return impBuilder({})
}

7
src/core/type.ts Normal file
View file

@ -0,0 +1,7 @@
import type {TypeName} from '@/interfaces/type'
// Dummy implementation for now
export function joinTypes(a: TypeName, b: TypeName) {
if (a === b) return a
return 'any'
}

5
src/index.ts Normal file
View file

@ -0,0 +1,5 @@
import {inspect} from 'node:util'
import * as specifications from './all'
console.log(inspect(specifications, {depth: 8, colors: true}))

View file

@ -0,0 +1,20 @@
import type {ClosureType, NaNType, RealType} from './type'
type UnOp<T> = (a: T) => T
type BinOp<T> = (a: T, B: T) => T
declare module "./type" {
interface CommonInterface<T, Aux> {
add: BinOp<T>
unaryMinus: UnOp<T>
conj: UnOp<T>
subtract: BinOp<T>
multiply: BinOp<T>
square: UnOp<T>
absquare: (a: T) => RealType<T>
reciprocal: UnOp<T>
divide: BinOp<T>
conservativeSqrt: (a: T) => (T | NaNType<T>)
sqrt: (a: T) => (T | ClosureType<T>)
}
}

View file

@ -0,0 +1,11 @@
import type {NaNType} from './type.ts'
export type BinaryPredicate<T> = (a: T, b: T) => boolean
declare module "./type" {
interface CommonInterface<T, Aux> {
equal: BinaryPredicate<T>
unequal: BinaryPredicate<T>
isnan: (a: T | NaNType<T>) => a is NaNType<T>
}
}

116
src/interfaces/type.ts Normal file
View file

@ -0,0 +1,116 @@
import {Configuration} from '@/core/Configuration'
/* First some type utilities: */
export type ToMapped<T> = {[K in keyof T]: T[K]}
export type UnionToIntersection<U> =
(U extends any ? (k: U) => void : never) extends ((k: infer I) => void)
? I : never
export type ValueIntersectionByKeyUnion<T, TKey extends keyof T> = {
[P in TKey]: (k: T[P]) => void
} [TKey] extends ((k: infer I) => void) ? I : never
// The following needs to be a type that is extended by
// the type of any function. Hopefully I have gotten this right.
export type AnyFunc = (...args: never[]) => unknown
// An ensemble of signatures of operations, for example, the CommonSignatures
// collected up by all of the modules
export type GenSigs = Record<string, AnyFunc>
/*****
* Every core math type has some associated types; they need
* to be published in the following interface. The key is the
* name of the type, and within the subinterface for that key,
* the type of the 'type' property is the actual TypeScript type
* we are associating the other properties to. To get an associated type,
* types are looked up by matching this 'type' property.
*
* Note the interface is generic with one parameter. To actually find
* the associated types of type T, instantiate the interface with type
* T. This mechanism deals with the fact that TypeScript doesn't really
* deal well with interfaces, some entries of which are generic and others
* are not.
****/
export interface AssociatedTypes<T> {
undefined: {
type: undefined
zero: undefined // The type of the zero of this type
one: undefined // The type of the multiplicative identity of this type
nan: undefined // The type of Not a Number of this type
real: undefined // The type of the real part of this type
closure: undefined // The type of the algebraic closure of this type
}
}
export type TypeName = string // Really should be some recursive definition,
// any key of AssociatedTypes that's not generic, or any key that is generic
// but instantatied by a TypeName. Not sure how to do that, so just go with
// any string for now.
type AssociatedTypeNames = keyof AssociatedTypes<unknown>['undefined']
type ALookup<T, Name extends AssociatedTypeNames> =
ValueIntersectionByKeyUnion<
{[K in keyof AssociatedTypes<T>]:
T extends AssociatedTypes<T>[K]['type']
? AssociatedTypes<T>[K][Name] : unknown
},
keyof AssociatedTypes<T>
>
// For everything to compile, zero and one must be subtypes of T:
export type ZeroType<T> = ALookup<T, 'zero'> & T
export type OneType<T> = ALookup<T, 'one'> & T
// But I believe 'nan' really might not be, like I think we will have to use
// number NaN for the nan of 'bigint', as it has nothing at all like NaN,
// so don't force it:
export type NaNType<T> = ALookup<T, 'nan'>
export type RealType<T> = ALookup<T, 'real'>
export type ClosureType<T> = ALookup<T, 'closure'>
/*****
* The typical signature for every operation needs to be published in the
* following interface. Each key is the name of an operation.
* The type of each key should be the function type that the operation would
* "normally" have, understanding that there may be exceptions, which will\
* be dealt with by another mechanism.
*
* Note that this interface is generic in two parameters, the second of which
* defaults to the first. These are slots for type parameters to the typical
* signatures, most of which have only one type parameter.
****/
export interface CommonInterface<T, Aux = T> {
zero: (a: T) => ZeroType<T>
one: (a: T) => OneType<T>
nan: (a: T | NaNType<T>) => NaNType<T>
re: (a: T) => RealType<T>
config: () => Configuration
convert: (from: T) => Aux
typeOf: (x: unknown) => TypeName
}
export type CommonSignature<T, Aux = T> = ToMapped<CommonInterface<T, Aux>>
export type SignatureKey<T, Aux = T> = keyof CommonSignature<T, Aux>
export function commonSignature<
K, T, Aux = T, CS extends GenSigs = CommonSignature<T, Aux>
>(): K extends keyof CS ? CS[K] : () => void {
return (() => undefined) as K extends keyof CS ? CS[K] : () => void
}
export type CommonReturn<
K, T, Aux = T, CS extends GenSigs = CommonSignature<T, Aux>
> = K extends keyof CS ? ReturnType<CS[K]> : void
export type Dependency<T, Aux = T> = {
[K in SignatureKey<T, Aux>]: Pick<CommonSignature<T, Aux>, K>
}
export type Dependencies<T, Names extends SignatureKey<T>, Aux = T> =
Pick<CommonSignature<T, Aux>, Names>

2
src/numbers/all.ts Normal file
View file

@ -0,0 +1,2 @@
export * as type_data from './type'
export * as arithmetic_functions from './arithmetic'

26
src/numbers/arithmetic.ts Normal file
View file

@ -0,0 +1,26 @@
import {implementations} from '@/core/Dispatcher'
import type {CommonSignature} from '@/interfaces/type'
const conservativeSqrt = (a: number) => isNaN(a) ? NaN : Math.sqrt(a)
export default implementations<CommonSignature<number>>()
.independent({
add: (a, b) => a + b,
unaryMinus: a => -a,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
absquare: a => a * a,
reciprocal: a => 1 / a,
divide: (a, b) => a / b,
conj: a => a,
conservativeSqrt })
.dependent({config: {}, complex: {}}, {
sqrt: dep => {
if (dep.config().predictable || !dep.complex) return conservativeSqrt
return a => {
if (isNaN(a)) return NaN
if (a >= 0) return Math.sqrt(a)
return dep.complex(0, Math.sqrt(-a))
}
}})
.ship()

31
src/numbers/type.ts Normal file
View file

@ -0,0 +1,31 @@
import type {Complex} from '@/Complex/type'
import {implementations} from '@/core/Dispatcher'
import type {CommonSignature} from '@/interfaces/type'
export const number_type = {
name: 'number',
before: ['Complex'],
test: (n: unknown): n is number => typeof n === 'number',
from: {string: (s: string) => +s }
}
declare module "@/interfaces/type" {
interface AssociatedTypes<T> {
number: {
type: number
zero: 0
one: 1
nan: typeof NaN
real: number
closure: Complex<number>
}
}
}
export default implementations<CommonSignature<number>>()
.independent({
zero: a => 0,
one: a => 1,
nan: a => NaN,
re: a => a
}).ship()