packages/defmulti/src/defmulti.ts
import type { IObjectOf, Maybe, Pair } from "@thi.ng/api";
import { unsupported } from "@thi.ng/errors/unsupported";
import type {
AncestorDefs,
DispatchFn,
DispatchFn1,
DispatchFn1O,
DispatchFn2,
DispatchFn2O,
DispatchFn3,
DispatchFn3O,
DispatchFn4,
DispatchFn4O,
DispatchFn5,
DispatchFn5O,
DispatchFn6,
DispatchFn6O,
DispatchFn7,
DispatchFn7O,
DispatchFn8,
DispatchFn8O,
Implementation,
Implementation1,
Implementation1O,
Implementation2,
Implementation2O,
Implementation3,
Implementation3O,
Implementation4,
Implementation4O,
Implementation5,
Implementation5O,
Implementation6,
Implementation6O,
Implementation7,
Implementation7O,
Implementation8,
Implementation8O,
MultiFn,
MultiFn1,
MultiFn1O,
MultiFn2,
MultiFn2O,
MultiFn3,
MultiFn3O,
MultiFn4,
MultiFn4O,
MultiFn5,
MultiFn5O,
MultiFn6,
MultiFn6O,
MultiFn7,
MultiFn7O,
MultiFn8,
MultiFn8O,
} from "./api.js";
import { LOGGER } from "./logger.js";
/**
* Unique symbol used for registering a default / fallback
* implementation.
*/
export const DEFAULT: unique symbol = Symbol();
/**
* Returns a new multi-dispatch function using the provided dispatch value
* function. Additionally, optional ancestor relationships (`rels`) and set of
* implementations (`impls`) can be given.
*
* @remarks
* The dispatcher can take any number of arguments and must produce a dispatch
* value (string, number or symbol) used to lookup an implementation. If found,
* the impl is called with the same args and its return value used as own return
* value. If no matching implementation is available, attempts to dispatch to
* DEFAULT impl. If none is registered, an error is thrown.
*
* `defmulti` provides generics for type checking up to 8 args (plus the return
* type) and the generics will also apply to all implementations. If more than 8
* args are required, `defmulti` will fall back to an untyped varargs solution.
*
* Implementations for different dispatch values can be added and removed
* dynamically by calling `.add(id, fn)`, `.addAll(impls)` or `.remove(id)` on
* the returned function. Each returns `true` if the operation was successful.
*/
export function defmulti<T>(
f: DispatchFn,
rels?: AncestorDefs,
impls?: IObjectOf<Implementation<T>>
): MultiFn<T>;
export function defmulti<A, T>(
f: DispatchFn1<A>,
rels?: AncestorDefs,
impls?: IObjectOf<Implementation1<A, T>>
): MultiFn1<A, T>;
export function defmulti<A, B, T>(
f: DispatchFn2<A, B>,
rels?: AncestorDefs,
impls?: IObjectOf<Implementation2<A, B, T>>
): MultiFn2<A, B, T>;
export function defmulti<A, B, T>(
f: DispatchFn1O<A, B>,
rels?: AncestorDefs,
impls?: IObjectOf<Implementation1O<A, B, T>>
): MultiFn1O<A, B, T>;
export function defmulti<A, B, C, T>(
f: DispatchFn3<A, B, C>,
rels?: AncestorDefs,
impls?: IObjectOf<Implementation3<A, B, C, T>>
): MultiFn3<A, B, C, T>;
export function defmulti<A, B, C, T>(
f: DispatchFn2O<A, B, C>,
rels?: AncestorDefs,
impls?: IObjectOf<Implementation2O<A, B, C, T>>
): MultiFn2O<A, B, C, T>;
export function defmulti<A, B, C, D, T>(
f: DispatchFn4<A, B, C, D>,
rels?: AncestorDefs,
impls?: IObjectOf<Implementation4<A, B, C, D, T>>
): MultiFn4<A, B, C, D, T>;
export function defmulti<A, B, C, D, T>(
f: DispatchFn3O<A, B, C, D>,
rels?: AncestorDefs,
impls?: IObjectOf<Implementation3O<A, B, C, D, T>>
): MultiFn3O<A, B, C, D, T>;
export function defmulti<A, B, C, D, E, T>(
f: DispatchFn5<A, B, C, D, E>,
rels?: AncestorDefs,
impls?: IObjectOf<Implementation5<A, B, C, D, E, T>>
): MultiFn5<A, B, C, D, E, T>;
export function defmulti<A, B, C, D, E, T>(
f: DispatchFn4O<A, B, C, D, E>,
rels?: AncestorDefs,
impls?: IObjectOf<Implementation4O<A, B, C, D, E, T>>
): MultiFn4O<A, B, C, D, E, T>;
export function defmulti<A, B, C, D, E, F, T>(
f: DispatchFn6<A, B, C, D, E, F>,
rels?: AncestorDefs,
impls?: IObjectOf<Implementation6<A, B, C, D, E, F, T>>
): MultiFn6<A, B, C, D, E, F, T>;
export function defmulti<A, B, C, D, E, F, T>(
f: DispatchFn5O<A, B, C, D, E, F>,
rels?: AncestorDefs,
impls?: IObjectOf<Implementation5O<A, B, C, D, E, F, T>>
): MultiFn5O<A, B, C, D, E, F, T>;
export function defmulti<A, B, C, D, E, F, G, T>(
f: DispatchFn7<A, B, C, D, E, F, G>,
rels?: AncestorDefs,
impls?: IObjectOf<Implementation7<A, B, C, D, E, F, G, T>>
): MultiFn7<A, B, C, D, E, F, G, T>;
export function defmulti<A, B, C, D, E, F, G, T>(
f: DispatchFn6O<A, B, C, D, E, F, G>,
rels?: AncestorDefs,
impls?: IObjectOf<Implementation6O<A, B, C, D, E, F, G, T>>
): MultiFn6O<A, B, C, D, E, F, G, T>;
export function defmulti<A, B, C, D, E, F, G, H, T>(
f: DispatchFn8<A, B, C, D, E, F, G, H>,
rels?: AncestorDefs,
impls?: IObjectOf<Implementation8<A, B, C, D, E, F, G, H, T>>
): MultiFn8<A, B, C, D, E, F, G, H, T>;
export function defmulti<A, B, C, D, E, F, G, H, T>(
f: DispatchFn7O<A, B, C, D, E, F, G, H>,
rels?: AncestorDefs,
impls?: IObjectOf<Implementation7O<A, B, C, D, E, F, G, H, T>>
): MultiFn7O<A, B, C, D, E, F, G, H, T>;
export function defmulti<A, B, C, D, E, F, G, H, I, T>(
f: DispatchFn8O<A, B, C, D, E, F, G, H, I>,
rels?: AncestorDefs,
impls?: IObjectOf<Implementation8O<A, B, C, D, E, F, G, H, I, T>>
): MultiFn8O<A, B, C, D, E, F, G, H, I, T>;
export function defmulti<T>(
f: any,
_rels?: AncestorDefs,
_impls?: IObjectOf<Implementation<T>>
) {
const impls: IObjectOf<Implementation<T>> = {};
const rels: IObjectOf<Set<PropertyKey>> = _rels ? makeRels(_rels) : {};
const fn: any = (...args: any[]) => {
const id = f(...args);
const g = impls[id] || findImpl(impls, rels, id) || impls[<any>DEFAULT];
return g
? g(...args)
: unsupported(`missing implementation for: "${id.toString()}"`);
};
fn.add = (id: PropertyKey, _impl: Implementation<T>) => {
if (impls[<any>id]) {
LOGGER.warn(`overwriting '${id.toString()}' impl`);
}
impls[<any>id] = _impl;
return true;
};
fn.addAll = (_impls: IObjectOf<Implementation<T>>) => {
let ok = true;
for (let id in _impls) {
ok = fn.add(id, _impls[id]) && ok;
}
DEFAULT in _impls && fn.setDefault(_impls[<any>DEFAULT]);
return ok;
};
fn.setDefault = (impl: Implementation<T>) => fn.add(DEFAULT, impl);
fn.remove = (id: PropertyKey) => {
if (!impls[<any>id]) return false;
delete impls[<any>id];
return true;
};
fn.callable = (...args: any[]) => {
const id = f(...args);
return !!(
impls[id] ||
findImpl(impls, rels, id) ||
impls[<any>DEFAULT]
);
};
fn.isa = (id: PropertyKey, parent: PropertyKey) => {
let val = rels[<any>id];
!val && (rels[<any>id] = val = new Set());
val.add(parent);
};
fn.impls = () => {
const res = new Map<PropertyKey, any>();
for (let id in impls) {
res.set(id, impls[id]);
}
for (let id in rels) {
const impl = findImpl(impls, rels, id);
if (impl) res.set(id, impl);
}
if (impls[<any>DEFAULT]) {
res.set(DEFAULT, impls[<any>DEFAULT]);
}
return res;
};
fn.implForID = (id: PropertyKey) =>
impls[String(id)] || findImpl(impls, rels, id) || impls[<any>DEFAULT];
fn.rels = () => rels;
fn.parents = (id: PropertyKey) => rels[<any>id];
fn.ancestors = (id: PropertyKey) =>
new Set<PropertyKey>(findAncestors([], rels, id));
fn.dependencies = function* (): IterableIterator<
Pair<PropertyKey, Maybe<PropertyKey>>
> {
for (let a in rels) {
for (let b of rels[a]) yield [a, b];
}
for (let id in impls) {
!rels[id] && (yield [id, undefined]);
}
};
_impls && fn.addAll(_impls);
return fn;
}
const findImpl = (
impls: IObjectOf<Implementation<any>>,
rels: IObjectOf<Set<PropertyKey>>,
id: PropertyKey
) => {
const parents = rels[<any>id];
if (!parents) return;
for (let p of parents) {
let impl: Implementation<any> =
impls[<any>p] || findImpl(impls, rels, p);
if (impl) return impl;
}
};
const findAncestors = (
acc: PropertyKey[],
rels: IObjectOf<Set<PropertyKey>>,
id: PropertyKey
) => {
const parents = rels[<any>id];
if (parents) {
for (let p of parents) {
acc.push(p);
findAncestors(acc, rels, p);
}
}
return acc;
};
const makeRels = (spec: AncestorDefs) => {
const rels: IObjectOf<Set<PropertyKey>> = {};
for (let k in spec) {
const val = spec[k];
rels[k] =
val instanceof Set
? val
: Array.isArray(val)
? new Set(val)
: new Set([val]);
}
return rels;
};