refactor: entirely new scheme for specifying return types
This commit is contained in:
parent
3fa216d1f4
commit
1eb73be2fa
@ -1,8 +1,10 @@
|
||||
import {ForType} from '../core/Dispatcher.js'
|
||||
import {ComplexReturn} from './type.js'
|
||||
import * as Complex from './native.js'
|
||||
|
||||
export {Complex}
|
||||
|
||||
declare module "../core/Dispatcher" {
|
||||
interface ImplementationTypes extends ForType<'Complex', typeof Complex> {}
|
||||
interface ReturnTypes<Params>
|
||||
extends ForType<'Complex', ComplexReturn<Params>> {}
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
import {joinTypes, typeOfDependency, Dependency} from '../core/Dispatcher.js'
|
||||
import {
|
||||
joinTypes, typeOfDependency, Dependency, BBinary, ImpType, ImpReturns
|
||||
} from '../core/Dispatcher.js'
|
||||
|
||||
export type Complex<T> = {re: T; im: T;}
|
||||
|
||||
@ -17,6 +19,16 @@ export const Complex_type = {
|
||||
}
|
||||
}
|
||||
|
||||
export const complex_unary = <T>(dep: Dependency<'zero', [T]>) =>
|
||||
(t: T) => ({re: t, im: dep.zero(t)})
|
||||
export const complex_binary = <T>(t: T, u: T) => ({re: t, im: u})
|
||||
type Binary<B> = [B, B]
|
||||
|
||||
export interface ComplexReturn<Params> {
|
||||
complex: Params extends [infer U] ? Complex<U> // unary case
|
||||
: Params extends BBinary<infer B> ? Complex<B> // binary case
|
||||
: never
|
||||
}
|
||||
|
||||
export const complex_unary =
|
||||
<T>(dep: Dependency<'zero', [T]>): ImpType<'complex', [T]> =>
|
||||
t => ({re: t, im: dep.zero(t)})
|
||||
export const complex_binary = <T>(t: T, u: T): ImpReturns<'complex', [T,T]> =>
|
||||
({re: t, im: u})
|
||||
|
@ -10,14 +10,68 @@
|
||||
type TypeName = string
|
||||
type Parameter = TypeName
|
||||
type Signature = Parameter[]
|
||||
type DependenciesType = Record<string, Function>
|
||||
|
||||
export interface ImplementationTypes {}
|
||||
export type typeOfDependency = {typeOf: (x: unknown) => TypeName}
|
||||
|
||||
// Helper for collecting implementations
|
||||
// (Really just suffixes the type name onto the keys of exports)
|
||||
export type ForType<T extends string, Exports> = keyof Exports extends string
|
||||
? {[K in keyof Exports as `${K}_${T}`]: Exports[K]}
|
||||
// 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 homogenous binary operation (comes up a lot)
|
||||
// Typical usage: `foo_impl: Params extends BBinary<infer B> ? B : never`
|
||||
// says that this implementation takes two arguments, both of type B, and
|
||||
// returns the same type.
|
||||
export type BBinary<B> = [B, B]
|
||||
|
||||
// Helper for collecting return types
|
||||
// (Really just adds the literal string Suffix onto the keys of interface IFace)
|
||||
export type ForType<Suffix extends string, IFace> = keyof IFace extends string
|
||||
? {[K in keyof IFace as `${K}_${Suffix}`]: IFace[K]}
|
||||
: never
|
||||
|
||||
//dummy implementation for now
|
||||
@ -26,27 +80,27 @@ export function joinTypes(a: TypeName, b: TypeName) {
|
||||
return 'any'
|
||||
}
|
||||
|
||||
/**
|
||||
* Build up to Dependency type lookup
|
||||
*/
|
||||
type DependenciesType = Record<string, Function>
|
||||
// Used to filter keys that match a given operation name
|
||||
type BeginsWith<Name extends string> = Name | `${Name}_${string}`
|
||||
|
||||
type FinalShape<FuncType> =
|
||||
FuncType extends (arg: DependenciesType) => Function
|
||||
? ReturnType<FuncType> : FuncType
|
||||
// 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>]
|
||||
|
||||
type BeginsWith<Name extends string> = `${Name}${string}`
|
||||
|
||||
type DependencyTypes<Ob, Name extends string, Params extends unknown[]> =
|
||||
{[K in keyof Ob]: K extends BeginsWith<Name>
|
||||
? FinalShape<Ob[K]> extends (...args: Params) => any
|
||||
? FinalShape<Ob[K]>
|
||||
: never
|
||||
: never}
|
||||
// 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]:
|
||||
DependencyTypes<ImplementationTypes, N, Params>[keyof ImplementationTypes]}
|
||||
{[N in Name]: ImpType<N, Params>}
|
||||
|
||||
// Now types used in the Dispatcher class itself
|
||||
|
||||
|
@ -1,8 +1,10 @@
|
||||
import {ForType} from '../core/Dispatcher.js'
|
||||
import {NumbersReturn} from './type.js'
|
||||
import * as numbers from './native.js'
|
||||
|
||||
export {numbers}
|
||||
|
||||
declare module "../core/Dispatcher" {
|
||||
interface ImplementationTypes extends ForType<'numbers', typeof numbers> {}
|
||||
interface ReturnTypes<Params>
|
||||
extends ForType<'numbers', NumbersReturn<Params>> {}
|
||||
}
|
||||
|
@ -1,18 +1,45 @@
|
||||
import {configDependency} from '../core/Config.js'
|
||||
import {Dependency} from '../core/Dispatcher.js'
|
||||
import {BBinary, Dependency, ImpType} from '../core/Dispatcher.js'
|
||||
import type {Complex} from '../Complex/type.js'
|
||||
|
||||
export const add = (a: number, b: number) => a + b
|
||||
export const unaryMinus = (a: number) => -a
|
||||
export const subtract = (a: number, b: number) => a - b
|
||||
export const multiply = (a: number, b: number) => a * b
|
||||
export const divide = (a: number, b: number) => a / b
|
||||
declare module "./type" {
|
||||
interface NumbersReturn<Params> {
|
||||
// This description loses information: some subtypes like NumInt or
|
||||
// Positive are closed under addition, but this says that the result
|
||||
// of add is just a number, not still of the reduced type
|
||||
add: Params extends BBinary<number> ? number : never
|
||||
// Whereas this one would preserve information, but would lie
|
||||
// because it claims all subtypes of number are closed under addition,
|
||||
// which is not true for `1 | 2 | 3`, for example.
|
||||
// add: Params extends BBinary<infer B>
|
||||
// ? B extends number ? B : never
|
||||
// : never
|
||||
//
|
||||
// Not sure how this will need to go when we introduce NumInt.
|
||||
unaryMinus: Params extends [number] ? number : never
|
||||
subtract: Params extends BBinary<number> ? number : never
|
||||
multiply: Params extends BBinary<number> ? number : never
|
||||
divide: Params extends BBinary<number> ? number : never
|
||||
// Best we can do for sqrt at compile time, since actual return
|
||||
// type depends on config. Not sure how this will play out
|
||||
// when we make a number-only bundle, but at least the import type
|
||||
// above for Complex<> does not lead to any emitted JavaScript.
|
||||
sqrt: Params extends [number] ? (number | Complex<number>) : never
|
||||
}
|
||||
}
|
||||
|
||||
export const add: ImpType<'add', [number, number]> = (a, b) => a + b
|
||||
export const unaryMinus: ImpType<'unaryMinus', [number]> = a => -a
|
||||
export const subtract: ImpType<'subtract', [number, number]> = (a, b) => a - b
|
||||
export const multiply: ImpType<'multiply', [number, number]> = (a, b) => a * b
|
||||
export const divide: ImpType<'divide', [number, number]> = (a, b) => a / b
|
||||
export const sqrt =
|
||||
(dep: configDependency
|
||||
& Dependency<'complex', [number, number]>) => {
|
||||
& Dependency<'complex', [number, number]>): ImpType<'sqrt', [number]> => {
|
||||
if (dep.config.predictable || !dep.complex) {
|
||||
return (a: number) => isNaN(a) ? NaN : Math.sqrt(a)
|
||||
return a => isNaN(a) ? NaN : Math.sqrt(a)
|
||||
}
|
||||
return (a: number) => {
|
||||
return a => {
|
||||
if (isNaN(a)) return NaN
|
||||
if (a >= 0) return Math.sqrt(a)
|
||||
return dep.complex(0, Math.sqrt(unaryMinus(a)))
|
||||
|
@ -1,7 +1,26 @@
|
||||
import {ImpType} from '../core/Dispatcher.js'
|
||||
|
||||
export const number_type = {
|
||||
before: ['Complex'],
|
||||
test: (n: unknown): n is number => typeof n === 'number',
|
||||
from: {string: s => +s}
|
||||
}
|
||||
|
||||
export const zero = (a: number) => 0
|
||||
|
||||
export interface NumbersReturn<Params> {
|
||||
// The following description of the return type of `zero` on a single
|
||||
// number argument has ended up unfortunately rather complicated. However,
|
||||
// it illustrates the typing is really working: Suppose we have a
|
||||
// `type Small = 1 | 2 | 3`. Then Small indeed extends number, but we
|
||||
// can't use the operation `zero(s: Small)` because zero is supposed to
|
||||
// return something of the same type as its argument, but there is no
|
||||
// zero in Small. Anyhow, in plain language the below says that given
|
||||
// one parameter of a subtype of number, as long as that subtype includes 0,
|
||||
// the zero operation returns a member of the type `0` (so we know even
|
||||
// at compile time that its value will be 0).
|
||||
zero: Params extends [infer T]
|
||||
? T extends number ? 0 extends T ? 0 : never : never
|
||||
: never
|
||||
}
|
||||
|
||||
export const zero: ImpType<'zero', [number]> = a => 0
|
||||
|
Loading…
Reference in New Issue
Block a user