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

115
.gitignore vendored
View File

@ -2,12 +2,11 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Editor backup files
*~
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
@ -17,116 +16,12 @@ pids
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Compiled code
build
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

108
README.md
View File

@ -1,3 +1,109 @@
# math5
Yet another math core prototype for a possible future of mathjs
Yet another math core prototype for a possible future of mathjs.
This project is a revision of the
[typocomath](https://code.studioinfinity.org/glen/typocomath) prototype
(which was the fourth in the series picomath, pocomath, typocomath, hence
the current name math5), preparing for an initial implementation of the
Dispatcher engine to assemble and run the methods specified in TypeScript.
Motivations for the refactor:
1. I observed that the `.d.ts` files that were being generated as a result
of the TypeScript compilation step did not contain sufficient type information
to see what each implementation/factory for each operation of the resulting
math module would do. This lack suggested that the TypeScript definitions of
the implementations and factories were not actually being fully typechecked.
2. I felt that there was still a significant amount of redundancy in the
implementation files. For example, in typocomath/src/numbers/arithmetic, it
reiterates for every arithmetic operation "foo" that "foo" implements the
number signature for the "foo" operation. It seemed like it would be
preferable to specify that this module is for the "number" type fewer times,
and not have to mention the operation name for each operation twice.
3. I did not love the creation of aliased operation names like "addReal" that
were actually implementations of the operation "add" but with different
signatures. I found that mechanism confusing.
You can verify that the new code compiles and generates implementation
information by cloning the repository and then running `pnpm install` and
`pnpm go`.
Outcomes of the refactor, corresponding to the motivations:
1a. You can browse the generated `build/**/*.d.ts` files to see that they
now contain full, detailed type information on every implementation and
factory, including the exact types of the dependencies.
1b. The TypeScript compiler now correctly detected (which it had not done in
typocomath) that the intermediate real square roots in the complex `sqrt`
implementation might be used even though they had come out to `NaN`. This
outcome is direct evidence that the TypeScript compiler is now type-checking
more strictly, so we are getting greater value from using TypeScript to
specify the operations' behavior. (In addition, it led to adding the `isnan`
predicate so that the code would compile.)
2. There is less repeated information. For example,
math5/src/numbers/arithmetic only mentions `number` twice, and only mentions
each operation once.
3. Implementations/factories are now only exported under their actual
operation names, just with different signatures specified. The default name
of a dependency is the name of the operation, but when you have dependencies
on a given operation with different signatures, you can name the dependency
arbitrarily and then specify which operation it is an instance of.
Other potential advantages of the refactor:
* Assembling the implementation specifications (the main task of which
is resolving and injecting dependencies) into a running math engine could
potentially work by parsing the `.d.ts` files as generated; we would not
necessarily need to instrument the typescript code with macros to generate
the additional information needed to correctly assemble the factories.
Some disadvantages of the refactor:
* The presentation of the code is slightly more verbose in places. The
primary cause of this is the switch to a "builder" interface for collecting
implementations/factories, as advised by TypeScript guru
[jcalz](https://stackoverflow.com/questions/79025259) in order to get
narrower type inference as desired. So for example in
src/Complex/arithmetic, every factory (a "dependent implementation", as
opposed to an "independent" one that has no dependencies) is wrapped in
its own call to `dependent(dependencySpecifiers, factories)`. And that
whole chain of `dependent` calls has to be kicked off with a call to
`implementations()` and wrapped up with a call to `ship()`. Of course, the
names of those functions could be changed, but it appears that currently
there is no way to avoid these wrappers if we want TypeScript to do narrow
type inference/typechecking.
* When one module is providing multiple implementations for the same
operation, but with different signatures, it must export multiple of these
bundles of implementations generated with an `implementation(). ... .ship()`
seequence, because each bundle can only contain an operation once. The names
of these bundles are arbitrary. I think this artificial division is a little
cumbersome/confusing. See src/Complex/arithmetic for an example, in which
there is a default export with the "common" signatures for operations, and a
`mixed` export with variants for `add` and `divide` that operate on a
Complex and an ordinary number.
* The notation for the desired signatures for dependencies can still be
a bit arcane/cumbersome. It's very simple when the desired dependency
consists of the common signature for that operation. But for more unusual
situations, it can become intricate. For example, in src/Complex/arithmetic,
the `absquare` (absolute value squared, an operation needed to define
division and square root) factory needs as a dependency the addition
operation on the return type of the `absquare` operation on the base type
of the Complex number. This has ended up being specified as:
```
add: {sig: commonSignature<'add', CommonReturn<'absquare', T>>()}
```
which is a bit of a mouthful. It's possible that better utilities for
expressing desired signatures could be devised; I'd want to wait until we had
collected a larger number of use cases before trying to design them. (If
this absquare case is essentially a one-off, it doesn't really matter
if it is a bit elaborate.)

31
package.json5 Normal file
View File

@ -0,0 +1,31 @@
{
name: 'math5',
version: '0.0.1',
description: 'Another prototype for a math core',
scripts: {
build: 'tsc && echo "{\\"type\\": \\"module\\"}" > build/package.json',
go: 'pnpm build && pnpm start',
start: 'node --experimental-loader tsc-module-loader build',
test: 'echo Error no test specified && exit 1',
},
packageManager: 'pnpm@9',
keywords: [
'math',
'algebra',
'typescript',
],
author: 'Glen Whitney',
license: 'Apache 2.0',
repository: {
type: 'git',
url: 'https://code.studioinfinity.org/glen/math5.git',
},
devDependencies: {
'@types/node': '^22.7.4',
typescript: '^5.6.2',
'undici-types': '^6.19.8',
},
dependencies: {
'tsc-module-loader': '^0.0.1',
},
}

103
pnpm-lock.yaml Normal file
View File

@ -0,0 +1,103 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
tsc-module-loader:
specifier: ^0.0.1
version: 0.0.1
devDependencies:
'@types/node':
specifier: ^22.7.4
version: 22.7.4
typescript:
specifier: ^5.6.2
version: 5.6.2
undici-types:
specifier: ^6.19.8
version: 6.19.8
packages:
'@types/node@22.7.4':
resolution: {integrity: sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==}
commonjs-extension-resolution-loader@0.1.0:
resolution: {integrity: sha512-XDCkM/cYIt1CfPs+LNX8nC2KKrzTx5AAlGLpx7A4BjWQCHR9LphDu9Iq5zXYf+PXhCkpLGBFiyiTnwmSnNxbWQ==}
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
is-core-module@2.15.1:
resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==}
engines: {node: '>= 0.4'}
path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
resolve@1.22.8:
resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==}
hasBin: true
supports-preserve-symlinks-flag@1.0.0:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
tsc-module-loader@0.0.1:
resolution: {integrity: sha512-3SIydFXw96jYU2imgULgIHKlUY8FnfDZlazvNmw4Umx/8qCwXsyDg0V2QOULf2Fw7zaI1Hbibh0mB8VzRZ/Ghg==}
typescript@5.6.2:
resolution: {integrity: sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==}
engines: {node: '>=14.17'}
hasBin: true
undici-types@6.19.8:
resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==}
snapshots:
'@types/node@22.7.4':
dependencies:
undici-types: 6.19.8
commonjs-extension-resolution-loader@0.1.0:
dependencies:
resolve: 1.22.8
function-bind@1.1.2: {}
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
is-core-module@2.15.1:
dependencies:
hasown: 2.0.2
path-parse@1.0.7: {}
resolve@1.22.8:
dependencies:
is-core-module: 2.15.1
path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0
supports-preserve-symlinks-flag@1.0.0: {}
tsc-module-loader@0.0.1:
dependencies:
commonjs-extension-resolution-loader: 0.1.0
resolve: 1.22.8
typescript@5.6.2: {}
undici-types@6.19.8: {}

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

16
tsconfig.json Normal file
View File

@ -0,0 +1,16 @@
{
"compilerOptions": {
"declaration": true,
"module": "esnext",
"outDir": "./build",
"paths": {
"@/*": ["./src/*"],
"undici-types": [
"./node_modules/undici-types/index.d.ts"
]
},
"rootDir": "./src",
"target": "esnext",
"types": ["node"]
}
}