/* 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 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 {} /***** 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 basic signature with concrete types export type Signature = 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 | `${Name}_${string}` // 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] // 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]: ImpType} // Now types used in the Dispatcher class itself type TypeSpecification = { before?: TypeName[], test: ((x: unknown) => boolean) | ((d: DependenciesType) => (x: unknown) => boolean), from: Record, infer?: (d: DependenciesType) => (z: unknown) => TypeName } type SpecObject = Record type SpecificationsGroup = Record export class Dispatcher { installSpecification( name: string, signature: InputSignature, returns: TypeName, dependencies: Record, 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) } } } } }