From 1eb73be2fa2a89d2aa7aa26bf146047e566253b5 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Wed, 21 Dec 2022 00:18:42 -0500 Subject: [PATCH] refactor: entirely new scheme for specifying return types --- src/Complex/all.ts | 4 +- src/Complex/type.ts | 20 ++++++-- src/core/Dispatcher.ts | 98 ++++++++++++++++++++++++++++++--------- src/numbers/all.ts | 4 +- src/numbers/arithmetic.ts | 45 ++++++++++++++---- src/numbers/type.ts | 21 ++++++++- 6 files changed, 154 insertions(+), 38 deletions(-) diff --git a/src/Complex/all.ts b/src/Complex/all.ts index 9d417b8..f5369ec 100644 --- a/src/Complex/all.ts +++ b/src/Complex/all.ts @@ -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 + extends ForType<'Complex', ComplexReturn> {} } diff --git a/src/Complex/type.ts b/src/Complex/type.ts index affbedc..a174c40 100644 --- a/src/Complex/type.ts +++ b/src/Complex/type.ts @@ -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 = {re: T; im: T;} @@ -17,6 +19,16 @@ export const Complex_type = { } } -export const complex_unary = (dep: Dependency<'zero', [T]>) => - (t: T) => ({re: t, im: dep.zero(t)}) -export const complex_binary = (t: T, u: T) => ({re: t, im: u}) +type Binary = [B, B] + +export interface ComplexReturn { + complex: Params extends [infer U] ? Complex // unary case + : Params extends BBinary ? Complex // binary case + : never +} + +export const complex_unary = + (dep: Dependency<'zero', [T]>): ImpType<'complex', [T]> => + t => ({re: t, im: dep.zero(t)}) +export const complex_binary = (t: T, u: T): ImpReturns<'complex', [T,T]> => + ({re: t, im: u}) diff --git a/src/core/Dispatcher.ts b/src/core/Dispatcher.ts index bed8f0d..46a8bff 100644 --- a/src/core/Dispatcher.ts +++ b/src/core/Dispatcher.ts @@ -10,14 +10,68 @@ type TypeName = string type Parameter = TypeName type Signature = Parameter[] +type DependenciesType = Record -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 = 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 {} + +/***** + 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 { + 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 : never + ... + ``` + In practice, each subdirectory corresponding to a type, like Complex, + defines an interface, like `ComplexReturn` 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 ? B : never` +// says that this implementation takes two arguments, both of type B, and +// returns the same type. +export type BBinary = [B, B] + +// Helper for collecting return types +// (Really just adds the literal string Suffix onto the keys of interface IFace) +export type ForType = 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 +// Used to filter keys that match a given operation name +type BeginsWith = Name | `${Name}_${string}` -type FinalShape = - FuncType extends (arg: DependenciesType) => Function - ? ReturnType : FuncType +// Look up the return type of an implementation based on its name +// and the parameters it takes +export type ImpReturns = + {[K in keyof ReturnTypes]: K extends BeginsWith + ? ReturnTypes[K] : never}[keyof ReturnTypes] -type BeginsWith = `${Name}${string}` - -type DependencyTypes = - {[K in keyof Ob]: K extends BeginsWith - ? FinalShape extends (...args: Params) => any - ? FinalShape - : never - : never} +// The type of an implementation (with dependencies satisfied, +// based on its name and the parameters it takes +export type ImpType = + (...args: Params) => ImpReturns +// 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 = - {[N in Name]: - DependencyTypes[keyof ImplementationTypes]} + {[N in Name]: ImpType} // Now types used in the Dispatcher class itself diff --git a/src/numbers/all.ts b/src/numbers/all.ts index 5aea220..b034f25 100644 --- a/src/numbers/all.ts +++ b/src/numbers/all.ts @@ -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 + extends ForType<'numbers', NumbersReturn> {} } diff --git a/src/numbers/arithmetic.ts b/src/numbers/arithmetic.ts index e78d9ec..bc9b862 100644 --- a/src/numbers/arithmetic.ts +++ b/src/numbers/arithmetic.ts @@ -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 { + // 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 : 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 + // ? 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 : never + multiply: Params extends BBinary ? number : never + divide: Params extends BBinary ? 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) : 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))) diff --git a/src/numbers/type.ts b/src/numbers/type.ts index 67dbd29..a32b791 100644 --- a/src/numbers/type.ts +++ b/src/numbers/type.ts @@ -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 { + // 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