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" { 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)))
} }

View File

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

View File

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

View File

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

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