refactor: Streamline types and signature specfications
The main mechanism for simplification was simply to assume that ZeroType<T> and OneType<T> will always be in T. That removed a lot of specialized typing, and presumably will be true in practice. Otherwise, removes extraneous type definitions and adds/clarifies a number of comments to hopefully make the scheme as clear as possible.
This commit is contained in:
parent
072b2a1f79
commit
6d63d23498
@ -5,8 +5,10 @@ import type {
|
|||||||
|
|
||||||
declare module "../interfaces/type" {
|
declare module "../interfaces/type" {
|
||||||
interface Operations<T> {
|
interface Operations<T> {
|
||||||
// TODO: Make Dispatcher collapse operations that start with the same
|
// TODO: Make Dispatcher collapse operations that match
|
||||||
// prefix up to a possible `_`
|
// after removing any `_...` suffixes; the following should be
|
||||||
|
// additional dispatches for add and divide, not separate
|
||||||
|
// operations, in the final mathjs bundle.
|
||||||
add_real: {params: [T, RealType<T>], returns: T}
|
add_real: {params: [T, RealType<T>], returns: T}
|
||||||
divide_real: {params: [T, RealType<T>], returns: T}
|
divide_real: {params: [T, RealType<T>], returns: T}
|
||||||
}
|
}
|
||||||
@ -70,12 +72,15 @@ export const divide =
|
|||||||
OpType<'divide', Complex<T>> =>
|
OpType<'divide', Complex<T>> =>
|
||||||
(w, z) => dep.multiply(w, dep.reciprocal(z))
|
(w, z) => dep.multiply(w, dep.reciprocal(z))
|
||||||
|
|
||||||
|
// The dependencies are slightly tricky here, because there are three types
|
||||||
|
// involved: Complex<T>, T, and RealType<T>, all of which might be different,
|
||||||
|
// and we have to get it straight which operations we need on each type, and
|
||||||
|
// in fact, we need `add_real` on both T and Complex<T>, hence the dependency
|
||||||
|
// with a custom name, not generated via Dependencies<...>
|
||||||
export const sqrt =
|
export const sqrt =
|
||||||
<T>(dep:
|
<T>(dep: Dependencies<'add' | 'equal' | 'conservativeSqrt' | 'unaryMinus',
|
||||||
Dependencies<
|
RealType<T>>
|
||||||
'conservativeSqrt' | 'add' | 'unaryMinus' | 'equal', RealType<T>>
|
& Dependencies<'zero' | 'add_real' | 'complex', T>
|
||||||
& Dependencies<'zero' | 'add_real', T>
|
|
||||||
& Dependencies<'complex', T | ZeroType<T>>
|
|
||||||
& Dependencies<'absquare' | 're' | 'divide_real', Complex<T>>
|
& Dependencies<'absquare' | 're' | 'divide_real', Complex<T>>
|
||||||
& {add_complex_real: OpType<'add_real', Complex<T>>}):
|
& {add_complex_real: OpType<'add_real', Complex<T>>}):
|
||||||
OpType<'sqrt', Complex<T>> =>
|
OpType<'sqrt', Complex<T>> =>
|
||||||
@ -84,7 +89,7 @@ export const sqrt =
|
|||||||
const r = dep.re(z)
|
const r = dep.re(z)
|
||||||
const negr = dep.unaryMinus(r)
|
const negr = dep.unaryMinus(r)
|
||||||
if (dep.equal(myabs, negr)) {
|
if (dep.equal(myabs, negr)) {
|
||||||
// pure imaginary square root; z.im already sero
|
// pure imaginary square root; z.im already zero
|
||||||
return dep.complex(
|
return dep.complex(
|
||||||
dep.zero(z.re), dep.add_real(z.im, dep.conservativeSqrt(negr)))
|
dep.zero(z.re), dep.add_real(z.im, dep.conservativeSqrt(negr)))
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
import {
|
import {joinTypes, typeOfDependency} from '../core/Dispatcher.js'
|
||||||
joinTypes, typeOfDependency, Dependency,
|
|
||||||
} from '../core/Dispatcher.js'
|
|
||||||
import type {
|
import type {
|
||||||
ZeroType, OneType, NaNType, Dependencies, OpType, OpReturns
|
ZeroType, OneType, NaNType, Dependencies, OpType, OpReturns
|
||||||
} from '../interfaces/type.js'
|
} from '../interfaces/type.js'
|
||||||
@ -15,7 +13,7 @@ export const Complex_type = {
|
|||||||
infer: (dep: typeOfDependency) =>
|
infer: (dep: typeOfDependency) =>
|
||||||
(z: Complex<unknown>) => joinTypes(dep.typeOf(z.re), dep.typeOf(z.im)),
|
(z: Complex<unknown>) => joinTypes(dep.typeOf(z.re), dep.typeOf(z.im)),
|
||||||
from: {
|
from: {
|
||||||
T: <T>(dep: Dependency<'zero', [T]>) => (t: T) =>
|
T: <T>(dep: Dependencies<'zero', T>) => (t: T) =>
|
||||||
({ re: t, im: dep.zero(t) }),
|
({ re: t, im: dep.zero(t) }),
|
||||||
Complex: <U, T>(dep: { convert: (from: U) => T }) =>
|
Complex: <U, T>(dep: { convert: (from: U) => T }) =>
|
||||||
(z: Complex<U>) => ({ re: dep.convert(z.re), im: dep.convert(z.im) })
|
(z: Complex<U>) => ({ re: dep.convert(z.re), im: dep.convert(z.im) })
|
||||||
@ -39,7 +37,7 @@ declare module "../interfaces/type" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const complex =
|
export const complex =
|
||||||
<T>(dep: Dependencies<'zero', T>): OpType<'complex', T | ZeroType<T>> =>
|
<T>(dep: Dependencies<'zero', T>): OpType<'complex', T> =>
|
||||||
(a, b) => ({re: a, im: b || dep.zero(a)})
|
(a, b) => ({re: a, im: b || dep.zero(a)})
|
||||||
|
|
||||||
export const zero =
|
export const zero =
|
||||||
|
@ -9,91 +9,19 @@
|
|||||||
|
|
||||||
type TypeName = string
|
type TypeName = string
|
||||||
type Parameter = TypeName
|
type Parameter = TypeName
|
||||||
type InputSignature = Parameter[]
|
type Signature = Parameter[]
|
||||||
type DependenciesType = Record<string, Function>
|
type DependenciesType = Record<string, Function>
|
||||||
|
|
||||||
|
// A "canned" dependency for a builtin function:
|
||||||
export type typeOfDependency = {typeOf: (x: unknown) => TypeName}
|
export type typeOfDependency = {typeOf: (x: unknown) => TypeName}
|
||||||
|
|
||||||
// All of the implementations must publish descriptions of their
|
// Utility needed in type definitions
|
||||||
// return types into the following interface, using the format
|
|
||||||
// described just below:
|
|
||||||
export interface ReturnTypes<Params> {}
|
|
||||||
|
|
||||||
/*****
|
|
||||||
To describe one implementation for a hypothetical operation `foo`, there
|
|
||||||
should be a property of the interface whose name starts with `foo` and whose
|
|
||||||
next character, if any, is an underscore. The type of this property
|
|
||||||
must be the return type of that implementation when Params matches the
|
|
||||||
parameter types of the implementation, and `never` otherwise.
|
|
||||||
Thus to describe an implementation that takes a number and a string and
|
|
||||||
returns a boolean, for example, you could write
|
|
||||||
```
|
|
||||||
declare module "Dispatcher" {
|
|
||||||
interface ReturnTypes<Params> {
|
|
||||||
foo_example: Params extends [number, string] ? boolean : never
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
If there is another, generic implementation that takes one argument
|
|
||||||
of any type and returns a Vector of that type, you can say
|
|
||||||
```
|
|
||||||
...
|
|
||||||
foo_generic: Params extends [infer T] ? Vector<T> : never
|
|
||||||
...
|
|
||||||
```
|
|
||||||
In practice, each subdirectory corresponding to a type, like Complex,
|
|
||||||
defines an interface, like `ComplexReturn<Params>` for the implementations
|
|
||||||
in that subdirectory, which can mostly be defined without suffixes because
|
|
||||||
there's typically just a single implementation within that domain.
|
|
||||||
Then the module responsible for collating all of the implementations for
|
|
||||||
that type inserts all of the properties of that interface into `ReturnTypes`
|
|
||||||
suitably suffixed to avoid collisions.
|
|
||||||
|
|
||||||
One might think that simply defining an implementation for `foo`
|
|
||||||
of type `(n: number, s: string) => boolean` would provide all of the same
|
|
||||||
information as the type of the key `foo_example` in the ReturnTypes
|
|
||||||
interface above, but in practice TypeScript has challenges in extracting
|
|
||||||
types relating to functions. (In particular, there is no
|
|
||||||
way to get the specialized return type of a generic function when it is
|
|
||||||
called on aguments whose specific types match the generic parameters.)
|
|
||||||
Hence the need for this additional mechanism to specify return types, in
|
|
||||||
a way readily suited for TypeScript type computations.
|
|
||||||
*****/
|
|
||||||
|
|
||||||
// Helpers for specifying signatures
|
|
||||||
|
|
||||||
// A basic signature with concrete types
|
|
||||||
export type Signature<CandidateParams, ActualParams, Returns> =
|
|
||||||
CandidateParams extends ActualParams ? Returns : never
|
|
||||||
|
|
||||||
//dummy implementation for now
|
//dummy implementation for now
|
||||||
export function joinTypes(a: TypeName, b: TypeName) {
|
export function joinTypes(a: TypeName, b: TypeName) {
|
||||||
if (a === b) return a
|
if (a === b) return a
|
||||||
return 'any'
|
return 'any'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Used to filter keys that match a given operation name
|
|
||||||
type BeginsWith<Name extends string> = Name | `${Name}_${string}`
|
|
||||||
|
|
||||||
// Look up the return type of an implementation based on its name
|
|
||||||
// and the parameters it takes
|
|
||||||
export type ImpReturns<Name extends string, Params> =
|
|
||||||
{[K in keyof ReturnTypes<Params>]: K extends BeginsWith<Name>
|
|
||||||
? ReturnTypes<Params>[K] : never}[keyof ReturnTypes<Params>]
|
|
||||||
|
|
||||||
// The type of an implementation (with dependencies satisfied,
|
|
||||||
// based on its name and the parameters it takes
|
|
||||||
export type ImpType<Name extends string, Params extends unknown[]> =
|
|
||||||
(...args: Params) => ImpReturns<Name, Params>
|
|
||||||
|
|
||||||
// The type of a dependency on an implementation based on its name
|
|
||||||
// and the parameters it takes (just a simple object with one property
|
|
||||||
// named the same as the operation, of value type equal to the type of
|
|
||||||
// that implementation. These can be `&`ed together in case of multiple
|
|
||||||
// dependencies:
|
|
||||||
export type Dependency<Name extends string, Params extends unknown[]> =
|
|
||||||
{[N in Name]: ImpType<N, Params>}
|
|
||||||
|
|
||||||
// Now types used in the Dispatcher class itself
|
// Now types used in the Dispatcher class itself
|
||||||
|
|
||||||
type TypeSpecification = {
|
type TypeSpecification = {
|
||||||
@ -110,9 +38,9 @@ type SpecificationsGroup = Record<string, SpecObject>
|
|||||||
export class Dispatcher {
|
export class Dispatcher {
|
||||||
installSpecification(
|
installSpecification(
|
||||||
name: string,
|
name: string,
|
||||||
signature: InputSignature,
|
signature: Signature,
|
||||||
returns: TypeName,
|
returns: TypeName,
|
||||||
dependencies: Record<string, InputSignature>,
|
dependencies: Record<string, Signature>,
|
||||||
behavior: Function // possible todo: constrain this type based
|
behavior: Function // possible todo: constrain this type based
|
||||||
// on the signature, return type, and dependencies. Not sure if
|
// on the signature, return type, and dependencies. Not sure if
|
||||||
// that's really possible, though.
|
// that's really possible, though.
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import type {Complex} from '../Complex/type.js'
|
import type {Complex} from '../Complex/type.js'
|
||||||
import type {RealType, WithConstants, NaNType} from './type.js'
|
import type {RealType} from './type.js'
|
||||||
|
|
||||||
type UnaryOperator<T> = {params: [T], returns: T}
|
type UnaryOperator<T> = {params: [T], returns: T}
|
||||||
type BinaryOperator<T> = {params: [T, T], returns: T}
|
type BinaryOperator<T> = {params: [T, T], returns: T}
|
||||||
@ -17,9 +17,7 @@ declare module "./type" {
|
|||||||
conservativeSqrt: UnaryOperator<T>
|
conservativeSqrt: UnaryOperator<T>
|
||||||
sqrt: {
|
sqrt: {
|
||||||
params: [T],
|
params: [T],
|
||||||
returns: T extends Complex<infer R>
|
returns: T extends Complex<any> ? T : T | Complex<T>
|
||||||
? Complex<R | ZeroType<R>>
|
|
||||||
: T | Complex<T>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
// Every typocomath type has some associated types; they need
|
/*****
|
||||||
// to be published as in the following interface. The key is the
|
* Every typocomath type has some associated types; they need
|
||||||
// name of the type, and within the subinterface for that key,
|
* to be published in the following interface. The key is the
|
||||||
// the type of the 'type' property is the actual TypeScript type
|
* name of the type, and within the subinterface for that key,
|
||||||
// we are associating the other properties to. Note the interface
|
* the type of the 'type' property is the actual TypeScript type
|
||||||
// is generic with one parameter, corresponding to the fact that
|
* we are associating the other properties to. Note the interface
|
||||||
// typocomath currently only allows types with a single generic parameter.
|
* is generic with one parameter, corresponding to the fact that
|
||||||
// This way, AssociatedTypes<SubType> can give the associated types
|
* typocomath currently only allows generic types with a single
|
||||||
// for a generic type instantiated with SubType. That's not necessary for
|
* generic parameter. This way, AssociatedTypes<SubType> can give the
|
||||||
// the 'undefined' type (or if you look in the `numbers` subdirectory,
|
* associated types for a generic type instantiated with SubType.
|
||||||
// the 'number' type either) or any concrete type, but that's OK, the
|
* That's not necessary for the 'undefined' type (or if you look in the
|
||||||
// generic parameter doesn't hurt in those cases.
|
* `numbers` subdirectory, the 'number' type) or any concrete type,
|
||||||
|
* but that's OK, the generic parameter doesn't hurt in those cases.
|
||||||
|
****/
|
||||||
|
|
||||||
export interface AssociatedTypes<T> {
|
export interface AssociatedTypes<T> {
|
||||||
undefined: {
|
undefined: {
|
||||||
type: undefined
|
type: undefined
|
||||||
@ -26,34 +29,41 @@ type ALookup<T, Name extends AssociatedTypeNames> = {
|
|||||||
T extends AssociatedTypes<T>[K]['type'] ? AssociatedTypes<T>[K][Name] : never
|
T extends AssociatedTypes<T>[K]['type'] ? AssociatedTypes<T>[K][Name] : never
|
||||||
}[keyof AssociatedTypes<T>]
|
}[keyof AssociatedTypes<T>]
|
||||||
|
|
||||||
export type ZeroType<T> = ALookup<T, 'zero'>
|
// For everything to compile, zero and one must be subtypes of T:
|
||||||
export type OneType<T> = ALookup<T, 'one'>
|
export type ZeroType<T> = ALookup<T, 'zero'> & T
|
||||||
export type WithConstants<T> = T | ZeroType<T> | OneType<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
|
||||||
|
// 'undefined' 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 NaNType<T> = ALookup<T, 'nan'>
|
||||||
export type RealType<T> = ALookup<T, 'real'>
|
export type RealType<T> = ALookup<T, 'real'>
|
||||||
|
|
||||||
// The global signature patterns for all operations need to be published in the
|
/*****
|
||||||
// following interface. Each key is the name of an operation (but note that
|
* The global signature patterns for all operations need to be published in the
|
||||||
// the Dispatcher will automatically merge operations that have the same
|
* following interface. Each key is the name of an operation (but note that
|
||||||
// name when the first underscore `_` and everything thereafter is stripped).
|
* the Dispatcher will automatically merge operations that have the same
|
||||||
// The type of each key should be an interface with two properties: 'params'
|
* name when the first underscore `_` and everything thereafter is stripped).
|
||||||
// whose type is the type of the parameter list for the operation, and
|
* The type of each key should be an interface with two properties: 'params'
|
||||||
// 'returns' whose type is the return type of the operation on those
|
* whose type is the type of the parameter list for the operation, and
|
||||||
// parameters. These types are generic in a parameter type T which should
|
* 'returns' whose type is the return type of the operation on those
|
||||||
// be interpreted as the type that the operation is supposed to "primarily"
|
* parameters. These types are generic in a parameter type T which should
|
||||||
// operate on, although note that some of the parameters and/or return types
|
* be interpreted as the type that the operation is supposed to "primarily"
|
||||||
// may depend on T rather than be exactly T.
|
* operate on, although note that some of the parameters and/or return types
|
||||||
// So note that the example 're' below provides essentially the same
|
* may depend on T rather than be exactly T.
|
||||||
// information that e.g.
|
* So note that the example 're' below provides essentially the same
|
||||||
// `type ReOp<T> = (t: T) => RealType<T>`
|
* information that e.g.
|
||||||
// would, but in a way that is much easier to manipulate in TypeScript,
|
* `type ReOp<T> = (t: T) => RealType<T>`
|
||||||
// and it records the name of the operation as 're' also by virtue of the
|
* would, but in a way that is much easier to manipulate in TypeScript,
|
||||||
// key 're' in the interface.
|
* and it records the name of the operation as 're' also by virtue of the
|
||||||
|
* key 're' in the interface.
|
||||||
|
****/
|
||||||
export interface Operations<T> {
|
export interface Operations<T> {
|
||||||
zero: {params: [WithConstants<T>], returns: ZeroType<T>}
|
zero: {params: [T], returns: ZeroType<T>}
|
||||||
one: {params: [WithConstants<T>], returns: OneType<T>}
|
one: {params: [T], returns: OneType<T>}
|
||||||
nan: {params: [T | NaNType<T>], returns: NaNType<T>}
|
// nan needs to be able to operate on its own output for everything
|
||||||
re: {params: [T], returns: RealType<T>}
|
// else to compile. That's why its parameter type is widened:
|
||||||
|
nan: {params: [T | NaNType<T>], returns: NaNType<T>}
|
||||||
|
re: {params: [T], returns: RealType<T>}
|
||||||
}
|
}
|
||||||
|
|
||||||
type OpKey = keyof Operations<unknown>
|
type OpKey = keyof Operations<unknown>
|
||||||
@ -62,5 +72,3 @@ export type OpReturns<Name extends OpKey, T> = Operations<T>[Name]['returns']
|
|||||||
export type OpType<Name extends OpKey, T> =
|
export type OpType<Name extends OpKey, T> =
|
||||||
(...args: Operations<T>[Name]['params']) => OpReturns<Name, T>
|
(...args: Operations<T>[Name]['params']) => OpReturns<Name, T>
|
||||||
export type Dependencies<Name extends OpKey, T> = {[K in Name]: OpType<K, T>}
|
export type Dependencies<Name extends OpKey, T> = {[K in Name]: OpType<K, T>}
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user