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:
Glen Whitney 2022-12-24 11:16:58 -05:00
parent 072b2a1f79
commit 6d63d23498
5 changed files with 69 additions and 132 deletions

View File

@ -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)))
}

View File

@ -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 =

View File

@ -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.

View File

@ -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>
}
}
}

View File

@ -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>}