refactor: entirely new scheme for specifying return types

This commit is contained in:
Glen Whitney 2022-12-21 00:18:42 -05:00
parent 3fa216d1f4
commit 1eb73be2fa
6 changed files with 154 additions and 38 deletions

View File

@ -1,8 +1,10 @@
import {ForType} from '../core/Dispatcher.js' import {ForType} from '../core/Dispatcher.js'
import {ComplexReturn} from './type.js'
import * as Complex from './native.js' import * as Complex from './native.js'
export {Complex} export {Complex}
declare module "../core/Dispatcher" { declare module "../core/Dispatcher" {
interface ImplementationTypes extends ForType<'Complex', typeof Complex> {} interface ReturnTypes<Params>
extends ForType<'Complex', ComplexReturn<Params>> {}
} }

View File

@ -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;} 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]>) => type Binary<B> = [B, B]
(t: T) => ({re: t, im: dep.zero(t)})
export const complex_binary = <T>(t: T, u: T) => ({re: t, im: u}) 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})

View File

@ -10,14 +10,68 @@
type TypeName = string type TypeName = string
type Parameter = TypeName type Parameter = TypeName
type Signature = Parameter[] type Signature = Parameter[]
type DependenciesType = Record<string, Function>
export interface ImplementationTypes {}
export type typeOfDependency = {typeOf: (x: unknown) => TypeName} export type typeOfDependency = {typeOf: (x: unknown) => TypeName}
// Helper for collecting implementations // All of the implementations must publish descriptions of their
// (Really just suffixes the type name onto the keys of exports) // return types into the following interface, using the format
export type ForType<T extends string, Exports> = keyof Exports extends string // described just below:
? {[K in keyof Exports as `${K}_${T}`]: Exports[K]} 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 : never
//dummy implementation for now //dummy implementation for now
@ -26,27 +80,27 @@ export function joinTypes(a: TypeName, b: TypeName) {
return 'any' return 'any'
} }
/** // Used to filter keys that match a given operation name
* Build up to Dependency type lookup type BeginsWith<Name extends string> = Name | `${Name}_${string}`
*/
type DependenciesType = Record<string, Function>
type FinalShape<FuncType> = // Look up the return type of an implementation based on its name
FuncType extends (arg: DependenciesType) => Function // and the parameters it takes
? ReturnType<FuncType> : FuncType 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}` // The type of an implementation (with dependencies satisfied,
// based on its name and the parameters it takes
type DependencyTypes<Ob, Name extends string, Params extends unknown[]> = export type ImpType<Name extends string, Params extends unknown[]> =
{[K in keyof Ob]: K extends BeginsWith<Name> (...args: Params) => ImpReturns<Name, Params>
? FinalShape<Ob[K]> extends (...args: Params) => any
? FinalShape<Ob[K]>
: never
: never}
// 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[]> = export type Dependency<Name extends string, Params extends unknown[]> =
{[N in Name]: {[N in Name]: ImpType<N, Params>}
DependencyTypes<ImplementationTypes, N, Params>[keyof ImplementationTypes]}
// Now types used in the Dispatcher class itself // Now types used in the Dispatcher class itself

View File

@ -1,8 +1,10 @@
import {ForType} from '../core/Dispatcher.js' import {ForType} from '../core/Dispatcher.js'
import {NumbersReturn} from './type.js'
import * as numbers from './native.js' import * as numbers from './native.js'
export {numbers} export {numbers}
declare module "../core/Dispatcher" { declare module "../core/Dispatcher" {
interface ImplementationTypes extends ForType<'numbers', typeof numbers> {} interface ReturnTypes<Params>
extends ForType<'numbers', NumbersReturn<Params>> {}
} }

View File

@ -1,18 +1,45 @@
import {configDependency} from '../core/Config.js' 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 declare module "./type" {
export const unaryMinus = (a: number) => -a interface NumbersReturn<Params> {
export const subtract = (a: number, b: number) => a - b // This description loses information: some subtypes like NumInt or
export const multiply = (a: number, b: number) => a * b // Positive are closed under addition, but this says that the result
export const divide = (a: number, b: number) => a / b // 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 = export const sqrt =
(dep: configDependency (dep: configDependency
& Dependency<'complex', [number, number]>) => { & Dependency<'complex', [number, number]>): ImpType<'sqrt', [number]> => {
if (dep.config.predictable || !dep.complex) { 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 (isNaN(a)) return NaN
if (a >= 0) return Math.sqrt(a) if (a >= 0) return Math.sqrt(a)
return dep.complex(0, Math.sqrt(unaryMinus(a))) return dep.complex(0, Math.sqrt(unaryMinus(a)))

View File

@ -1,7 +1,26 @@
import {ImpType} from '../core/Dispatcher.js'
export const number_type = { export const number_type = {
before: ['Complex'], before: ['Complex'],
test: (n: unknown): n is number => typeof n === 'number', test: (n: unknown): n is number => typeof n === 'number',
from: {string: s => +s} 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