typocomath/src/core/Dispatcher.ts

153 lines
5.8 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
// types into the following interface, using the format
// described just below:
export interface ImpTypes<T> {}
/*****
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 type of that implementation when T matches the
first parameter of the implementation.
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 ImpTypes<T> {
foo_example: (a: number, b: string) => boolean
}
}
```
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: (a: T) => Vector<T>
...
```
In practice, each subdirectory corresponding to a type, like Complex,
defines an interface, like `ComplexImpTypes<T>` 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.
Note that if the type is not generic, it will not bother with the generic
parameter in its subdirectory ImpTypes interface.
And note again, that for generic types, the type parameter of ImpTypes
_must_ match the type of the **first** argument of the operation. Hence,
choose argument order so that you can infer all the other parameter types
and the return type from the type of the first argument.
*****/
// 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
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}`
export type RawDependency<Name extends string, Params extends unknown[]> =
{[K in keyof ImpTypes<Params[0]>]: K extends BeginsWith<Name>
? ImpTypes<Params[0]>[K] extends (...args: Params) => any
? ImpTypes<Params[0]>[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[]> =
RawDependency<Name, Params>[keyof ImpTypes<Params[0]>]
// 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>}
// Look up the return type of an implementation based on its name
// and the parameters it takes
export type ImpReturns<Name extends string, Params extends unknown[]> =
ReturnType<ImpType<Name, 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)
}
}
}
}
}