148 lines
5.9 KiB
TypeScript
148 lines
5.9 KiB
TypeScript
/* A Dispatcher is a collection of operations that do run-time
|
|
* dispatch on the types of their arguments. Thus, every individual
|
|
* method is like a typed-function (from the library by that name),
|
|
* but they can depend on one another and on ona another's implementations
|
|
* for specific types (including their own).
|
|
*/
|
|
|
|
// First helper types and functions for the Dispatcher
|
|
|
|
type TypeName = string
|
|
type Parameter = TypeName
|
|
type InputSignature = Parameter[]
|
|
type DependenciesType = Record<string, 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
|
|
|
|
//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 = {
|
|
before?: TypeName[],
|
|
test: ((x: unknown) => boolean)
|
|
| ((d: DependenciesType) => (x: unknown) => boolean),
|
|
from: Record<TypeName, Function>,
|
|
infer?: (d: DependenciesType) => (z: unknown) => TypeName
|
|
}
|
|
|
|
type SpecObject = Record<string, Function | TypeSpecification>
|
|
type SpecificationsGroup = Record<string, SpecObject>
|
|
|
|
export class Dispatcher {
|
|
installSpecification(
|
|
name: string,
|
|
signature: InputSignature,
|
|
returns: TypeName,
|
|
dependencies: Record<string, InputSignature>,
|
|
behavior: Function // possible todo: constrain this type based
|
|
// on the signature, return type, and dependencies. Not sure if
|
|
// that's really possible, though.
|
|
) {
|
|
console.log('Pretending to install', name, signature, '=>', returns)
|
|
//TODO: implement me
|
|
}
|
|
installType(name: TypeName, typespec: TypeSpecification) {
|
|
console.log('Pretending to install type', name, typespec)
|
|
//TODO: implement me
|
|
}
|
|
constructor(collection: SpecificationsGroup) {
|
|
for (const key in collection) {
|
|
console.log('Working on', key)
|
|
for (const identifier in collection[key]) {
|
|
console.log('Handling', key, ':', identifier)
|
|
const parts = identifier.split('_')
|
|
if (parts[parts.length - 1] === 'type') {
|
|
parts.pop()
|
|
const name = parts.join('_')
|
|
this.installType(
|
|
name, collection[key][identifier] as TypeSpecification)
|
|
} else {
|
|
const name = parts[0]
|
|
this.installSpecification(
|
|
name, ['dunno'], 'unsure', {},
|
|
collection[key][identifier] as Function)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|