feat: Narrow tsc typing of operation dependencies/implementations
This commit is contained in:
parent
90b66dc863
commit
f575582879
115
.gitignore
vendored
115
.gitignore
vendored
@ -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
108
README.md
@ -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
31
package.json5
Normal 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
103
pnpm-lock.yaml
Normal 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
2
src/Complex/all.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * as type_data from './type'
|
||||
export * as arithmetic_functions from './arithmetic'
|
152
src/Complex/arithmetic.ts
Normal file
152
src/Complex/arithmetic.ts
Normal 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
71
src/Complex/type.ts
Normal 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
2
src/all.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * as numbers from '@/numbers/all'
|
||||
export * as Complex from '@/Complex/all'
|
4
src/core/Configuration.ts
Normal file
4
src/core/Configuration.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export type Configuration = {
|
||||
epsilon: number
|
||||
predictable: boolean
|
||||
}
|
201
src/core/Dispatcher.ts
Normal file
201
src/core/Dispatcher.ts
Normal 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
7
src/core/type.ts
Normal 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
5
src/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import {inspect} from 'node:util'
|
||||
|
||||
import * as specifications from './all'
|
||||
|
||||
console.log(inspect(specifications, {depth: 8, colors: true}))
|
20
src/interfaces/arithmetic.ts
Normal file
20
src/interfaces/arithmetic.ts
Normal 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>)
|
||||
}
|
||||
}
|
11
src/interfaces/relational.ts
Normal file
11
src/interfaces/relational.ts
Normal 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
116
src/interfaces/type.ts
Normal 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
2
src/numbers/all.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * as type_data from './type'
|
||||
export * as arithmetic_functions from './arithmetic'
|
26
src/numbers/arithmetic.ts
Normal file
26
src/numbers/arithmetic.ts
Normal 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
31
src/numbers/type.ts
Normal 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
16
tsconfig.json
Normal 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"]
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user