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" {
|
||||
interface Operations<T> {
|
||||
// TODO: Make Dispatcher collapse operations that start with the same
|
||||
// prefix up to a possible `_`
|
||||
// TODO: Make Dispatcher collapse operations that match
|
||||
// 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}
|
||||
divide_real: {params: [T, RealType<T>], returns: T}
|
||||
}
|
||||
@ -70,12 +72,15 @@ export const divide =
|
||||
OpType<'divide', Complex<T>> =>
|
||||
(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 =
|
||||
<T>(dep:
|
||||
Dependencies<
|
||||
'conservativeSqrt' | 'add' | 'unaryMinus' | 'equal', RealType<T>>
|
||||
& Dependencies<'zero' | 'add_real', T>
|
||||
& Dependencies<'complex', T | ZeroType<T>>
|
||||
<T>(dep: Dependencies<'add' | 'equal' | 'conservativeSqrt' | 'unaryMinus',
|
||||
RealType<T>>
|
||||
& Dependencies<'zero' | 'add_real' | 'complex', T>
|
||||
& Dependencies<'absquare' | 're' | 'divide_real', Complex<T>>
|
||||
& {add_complex_real: OpType<'add_real', Complex<T>>}):
|
||||
OpType<'sqrt', Complex<T>> =>
|
||||
@ -84,7 +89,7 @@ export const sqrt =
|
||||
const r = dep.re(z)
|
||||
const negr = dep.unaryMinus(r)
|
||||
if (dep.equal(myabs, negr)) {
|
||||
// pure imaginary square root; z.im already sero
|
||||
// pure imaginary square root; z.im already zero
|
||||
return dep.complex(
|
||||
dep.zero(z.re), dep.add_real(z.im, dep.conservativeSqrt(negr)))
|
||||
}
|
||||
|
@ -1,6 +1,4 @@
|
||||
import {
|
||||
joinTypes, typeOfDependency, Dependency,
|
||||
} from '../core/Dispatcher.js'
|
||||
import {joinTypes, typeOfDependency} from '../core/Dispatcher.js'
|
||||
import type {
|
||||
ZeroType, OneType, NaNType, Dependencies, OpType, OpReturns
|
||||
} from '../interfaces/type.js'
|
||||
@ -15,7 +13,7 @@ export const Complex_type = {
|
||||
infer: (dep: typeOfDependency) =>
|
||||
(z: Complex<unknown>) => joinTypes(dep.typeOf(z.re), dep.typeOf(z.im)),
|
||||
from: {
|
||||
T: <T>(dep: Dependency<'zero', [T]>) => (t: T) =>
|
||||
T: <T>(dep: Dependencies<'zero', T>) => (t: T) =>
|
||||
({ re: t, im: dep.zero(t) }),
|
||||
Complex: <U, T>(dep: { convert: (from: U) => T }) =>
|
||||
(z: Complex<U>) => ({ re: dep.convert(z.re), im: dep.convert(z.im) })
|
||||
@ -39,7 +37,7 @@ declare module "../interfaces/type" {
|
||||
}
|
||||
|
||||
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)})
|
||||
|
||||
export const zero =
|
||||
|
@ -9,91 +9,19 @@
|
||||
|
||||
type TypeName = string
|
||||
type Parameter = TypeName
|
||||
type InputSignature = Parameter[]
|
||||
type Signature = Parameter[]
|
||||
type DependenciesType = Record<string, Function>
|
||||
|
||||
// A "canned" dependency for a builtin function:
|
||||
export type typeOfDependency = {typeOf: (x: unknown) => TypeName}
|
||||
|
||||
// All of the implementations must publish descriptions of their
|
||||
// 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
|
||||
|
||||
// Utility needed in type definitions
|
||||
//dummy implementation for now
|
||||
export function joinTypes(a: TypeName, b: TypeName) {
|
||||
if (a === b) return a
|
||||
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
|
||||
|
||||
type TypeSpecification = {
|
||||
@ -110,9 +38,9 @@ type SpecificationsGroup = Record<string, SpecObject>
|
||||
export class Dispatcher {
|
||||
installSpecification(
|
||||
name: string,
|
||||
signature: InputSignature,
|
||||
signature: Signature,
|
||||
returns: TypeName,
|
||||
dependencies: Record<string, InputSignature>,
|
||||
dependencies: Record<string, Signature>,
|
||||
behavior: Function // possible todo: constrain this type based
|
||||
// on the signature, return type, and dependencies. Not sure if
|
||||
// that's really possible, though.
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 BinaryOperator<T> = {params: [T, T], returns: T}
|
||||
@ -17,9 +17,7 @@ declare module "./type" {
|
||||
conservativeSqrt: UnaryOperator<T>
|
||||
sqrt: {
|
||||
params: [T],
|
||||
returns: T extends Complex<infer R>
|
||||
? Complex<R | ZeroType<R>>
|
||||
: T | Complex<T>
|
||||
returns: T extends Complex<any> ? T : 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
|
||||
// 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. Note the interface
|
||||
// is generic with one parameter, corresponding to the fact that
|
||||
// typocomath currently only allows types with a single generic parameter.
|
||||
// This way, AssociatedTypes<SubType> can give the associated types
|
||||
// for a generic type instantiated with SubType. That's not necessary for
|
||||
// the 'undefined' type (or if you look in the `numbers` subdirectory,
|
||||
// the 'number' type either) or any concrete type, but that's OK, the
|
||||
// generic parameter doesn't hurt in those cases.
|
||||
/*****
|
||||
* Every typocomath 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. Note the interface
|
||||
* is generic with one parameter, corresponding to the fact that
|
||||
* typocomath currently only allows generic types with a single
|
||||
* generic parameter. This way, AssociatedTypes<SubType> can give the
|
||||
* associated types for a generic type instantiated with SubType.
|
||||
* That's not necessary for the 'undefined' type (or if you look in the
|
||||
* `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> {
|
||||
undefined: {
|
||||
type: undefined
|
||||
@ -26,34 +29,41 @@ type ALookup<T, Name extends AssociatedTypeNames> = {
|
||||
T extends AssociatedTypes<T>[K]['type'] ? AssociatedTypes<T>[K][Name] : never
|
||||
}[keyof AssociatedTypes<T>]
|
||||
|
||||
export type ZeroType<T> = ALookup<T, 'zero'>
|
||||
export type OneType<T> = ALookup<T, 'one'>
|
||||
export type WithConstants<T> = T | ZeroType<T> | OneType<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
|
||||
// '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 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 Dispatcher will automatically merge operations that have the same
|
||||
// name when the first underscore `_` and everything thereafter is stripped).
|
||||
// The type of each key should be an interface with two properties: 'params'
|
||||
// whose type is the type of the parameter list for the operation, and
|
||||
// 'returns' whose type is the return type of the operation on those
|
||||
// parameters. These types are generic in a parameter type T which should
|
||||
// be interpreted as the type that the operation is supposed to "primarily"
|
||||
// operate on, although note that some of the parameters and/or return types
|
||||
// may depend on T rather than be exactly T.
|
||||
// So note that the example 're' below provides essentially the same
|
||||
// information that e.g.
|
||||
// `type ReOp<T> = (t: T) => RealType<T>`
|
||||
// would, but in a way that is much easier to manipulate in TypeScript,
|
||||
// and it records the name of the operation as 're' also by virtue of the
|
||||
// key 're' in the interface.
|
||||
/*****
|
||||
* 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 Dispatcher will automatically merge operations that have the same
|
||||
* name when the first underscore `_` and everything thereafter is stripped).
|
||||
* The type of each key should be an interface with two properties: 'params'
|
||||
* whose type is the type of the parameter list for the operation, and
|
||||
* 'returns' whose type is the return type of the operation on those
|
||||
* parameters. These types are generic in a parameter type T which should
|
||||
* be interpreted as the type that the operation is supposed to "primarily"
|
||||
* operate on, although note that some of the parameters and/or return types
|
||||
* may depend on T rather than be exactly T.
|
||||
* So note that the example 're' below provides essentially the same
|
||||
* information that e.g.
|
||||
* `type ReOp<T> = (t: T) => RealType<T>`
|
||||
* would, but in a way that is much easier to manipulate in TypeScript,
|
||||
* and it records the name of the operation as 're' also by virtue of the
|
||||
* key 're' in the interface.
|
||||
****/
|
||||
export interface Operations<T> {
|
||||
zero: {params: [WithConstants<T>], returns: ZeroType<T>}
|
||||
one: {params: [WithConstants<T>], returns: OneType<T>}
|
||||
nan: {params: [T | NaNType<T>], returns: NaNType<T>}
|
||||
re: {params: [T], returns: RealType<T>}
|
||||
zero: {params: [T], returns: ZeroType<T>}
|
||||
one: {params: [T], returns: OneType<T>}
|
||||
// nan needs to be able to operate on its own output for everything
|
||||
// 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>
|
||||
@ -62,5 +72,3 @@ export type OpReturns<Name extends OpKey, T> = Operations<T>[Name]['returns']
|
||||
export type OpType<Name extends OpKey, T> =
|
||||
(...args: Operations<T>[Name]['params']) => OpReturns<Name, T>
|
||||
export type Dependencies<Name extends OpKey, T> = {[K in Name]: OpType<K, T>}
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user